first commit

This commit is contained in:
2020-06-08 23:57:36 +02:00
commit 6277454f7a
16057 changed files with 1715382 additions and 0 deletions
@@ -0,0 +1,55 @@
<?php
namespace Drupal\taxonomy\Controller;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Controller\ControllerBase;
use Drupal\taxonomy\TermInterface;
use Drupal\taxonomy\VocabularyInterface;
/**
* Provides route responses for taxonomy.module.
*/
class TaxonomyController extends ControllerBase {
/**
* Returns a form to add a new term to a vocabulary.
*
* @param \Drupal\taxonomy\VocabularyInterface $taxonomy_vocabulary
* The vocabulary this term will be added to.
*
* @return array
* The taxonomy term add form.
*/
public function addForm(VocabularyInterface $taxonomy_vocabulary) {
$term = $this->entityTypeManager()->getStorage('taxonomy_term')->create(['vid' => $taxonomy_vocabulary->id()]);
return $this->entityFormBuilder()->getForm($term);
}
/**
* Route title callback.
*
* @param \Drupal\taxonomy\VocabularyInterface $taxonomy_vocabulary
* The vocabulary.
*
* @return string
* The vocabulary label as a render array.
*/
public function vocabularyTitle(VocabularyInterface $taxonomy_vocabulary) {
return ['#markup' => $taxonomy_vocabulary->label(), '#allowed_tags' => Xss::getHtmlTagList()];
}
/**
* Route title callback.
*
* @param \Drupal\taxonomy\TermInterface $taxonomy_term
* The taxonomy term.
*
* @return array
* The term label as a render array.
*/
public function termTitle(TermInterface $taxonomy_term) {
return ['#markup' => $taxonomy_term->getName(), '#allowed_tags' => Xss::getHtmlTagList()];
}
}
@@ -0,0 +1,77 @@
<?php
namespace Drupal\taxonomy\Entity\Routing;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
use Symfony\Component\Routing\Route;
class VocabularyRouteProvider extends AdminHtmlRouteProvider {
/**
* {@inheritdoc}
*/
public function getRoutes(EntityTypeInterface $entity_type) {
$collection = parent::getRoutes($entity_type);
if ($reset_page_route = $this->getResetPageRoute($entity_type)) {
$collection->add("entity.taxonomy_vocabulary.reset_form", $reset_page_route);
}
if ($overview_page_route = $this->getOverviewPageRoute($entity_type)) {
$collection->add("entity.taxonomy_vocabulary.overview_form", $overview_page_route);
}
return $collection;
}
/**
* {@inheritdoc}
*/
protected function getCollectionRoute(EntityTypeInterface $entity_type) {
if ($route = parent::getCollectionRoute($entity_type)) {
$route->setRequirement('_permission', 'access taxonomy overview+administer taxonomy');
return $route;
}
}
/**
* Gets the reset page route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getResetPageRoute(EntityTypeInterface $entity_type) {
$route = new Route('/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/reset');
$route->setDefault('_entity_form', 'taxonomy_vocabulary.reset');
$route->setDefault('_title', 'Reset');
$route->setRequirement('_permission', $entity_type->getAdminPermission());
$route->setOption('_admin_route', TRUE);
return $route;
}
/**
* Gets the overview page route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getOverviewPageRoute(EntityTypeInterface $entity_type) {
$route = new Route('/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/overview');
$route->setDefault('_title_callback', 'Drupal\taxonomy\Controller\TaxonomyController::vocabularyTitle');
$route->setDefault('_form', 'Drupal\taxonomy\Form\OverviewTerms');
$route->setRequirement('_entity_access', 'taxonomy_vocabulary.access taxonomy overview');
$route->setOption('_admin_route', TRUE);
return $route;
}
}
@@ -0,0 +1,290 @@
<?php
namespace Drupal\taxonomy\Entity;
use Drupal\Core\Entity\EditorialContentEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\taxonomy\TermInterface;
use Drupal\user\StatusItem;
/**
* Defines the taxonomy term entity.
*
* @ContentEntityType(
* id = "taxonomy_term",
* label = @Translation("Taxonomy term"),
* label_collection = @Translation("Taxonomy terms"),
* label_singular = @Translation("taxonomy term"),
* label_plural = @Translation("taxonomy terms"),
* label_count = @PluralTranslation(
* singular = "@count taxonomy term",
* plural = "@count taxonomy terms",
* ),
* bundle_label = @Translation("Vocabulary"),
* handlers = {
* "storage" = "Drupal\taxonomy\TermStorage",
* "storage_schema" = "Drupal\taxonomy\TermStorageSchema",
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "list_builder" = "Drupal\Core\Entity\EntityListBuilder",
* "access" = "Drupal\taxonomy\TermAccessControlHandler",
* "views_data" = "Drupal\taxonomy\TermViewsData",
* "form" = {
* "default" = "Drupal\taxonomy\TermForm",
* "delete" = "Drupal\taxonomy\Form\TermDeleteForm"
* },
* "translation" = "Drupal\taxonomy\TermTranslationHandler"
* },
* base_table = "taxonomy_term_data",
* data_table = "taxonomy_term_field_data",
* revision_table = "taxonomy_term_revision",
* revision_data_table = "taxonomy_term_field_revision",
* uri_callback = "taxonomy_term_uri",
* translatable = TRUE,
* entity_keys = {
* "id" = "tid",
* "revision" = "revision_id",
* "bundle" = "vid",
* "label" = "name",
* "langcode" = "langcode",
* "uuid" = "uuid",
* "published" = "status",
* },
* revision_metadata_keys = {
* "revision_user" = "revision_user",
* "revision_created" = "revision_created",
* "revision_log_message" = "revision_log_message",
* },
* bundle_entity_type = "taxonomy_vocabulary",
* field_ui_base_route = "entity.taxonomy_vocabulary.overview_form",
* common_reference_target = TRUE,
* links = {
* "canonical" = "/taxonomy/term/{taxonomy_term}",
* "delete-form" = "/taxonomy/term/{taxonomy_term}/delete",
* "edit-form" = "/taxonomy/term/{taxonomy_term}/edit",
* "create" = "/taxonomy/term",
* },
* permission_granularity = "bundle",
* constraints = {
* "TaxonomyHierarchy" = {}
* }
* )
*/
class Term extends EditorialContentEntityBase implements TermInterface {
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
// See if any of the term's children are about to be become orphans.
$orphans = [];
/** @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.
if ($parent->count()) {
$child->save();
}
else {
$orphans[] = $child;
}
}
}
}
if (!empty($orphans)) {
$storage->delete($orphans);
}
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
// Terms with no parents are mandatory children of <root>.
if (!$this->get('parent')->count()) {
$this->parent->target_id = 0;
}
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
$fields = parent::baseFieldDefinitions($entity_type);
// @todo Remove the usage of StatusItem in
// https://www.drupal.org/project/drupal/issues/2936864.
$fields['status']->getItemDefinition()->setClass(StatusItem::class);
$fields['tid']->setLabel(t('Term ID'))
->setDescription(t('The term ID.'));
$fields['uuid']->setDescription(t('The term UUID.'));
$fields['status']
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
'settings' => [
'display_label' => TRUE,
],
'weight' => 100,
])
->setDisplayConfigurable('form', TRUE);
$fields['vid']->setLabel(t('Vocabulary'))
->setDescription(t('The vocabulary to which the term is assigned.'));
$fields['langcode']->setDescription(t('The term language code.'));
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setRequired(TRUE)
->setSetting('max_length', 255)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
])
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -5,
])
->setDisplayConfigurable('form', TRUE);
$fields['description'] = BaseFieldDefinition::create('text_long')
->setLabel(t('Description'))
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'text_default',
'weight' => 0,
])
->setDisplayConfigurable('view', TRUE)
->setDisplayOptions('form', [
'type' => 'text_textfield',
'weight' => 0,
])
->setDisplayConfigurable('form', TRUE);
$fields['weight'] = BaseFieldDefinition::create('integer')
->setLabel(t('Weight'))
->setDescription(t('The weight of this term in relation to other terms.'))
->setDefaultValue(0);
$fields['parent'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Term Parents'))
->setDescription(t('The parents of this term.'))
->setSetting('target_type', 'taxonomy_term')
->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED);
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the term was last edited.'))
->setTranslatable(TRUE)
->setRevisionable(TRUE);
// @todo Keep this field hidden until we have a revision UI for terms.
// @see https://www.drupal.org/project/drupal/issues/2936995
$fields['revision_log_message']->setDisplayOptions('form', [
'region' => 'hidden',
]);
return $fields;
}
/**
* {@inheritdoc}
*/
public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
// Only terms in the same bundle can be a parent.
$fields['parent'] = clone $base_field_definitions['parent'];
$fields['parent']->setSetting('handler_settings', ['target_bundles' => [$bundle => $bundle]]);
return $fields;
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->get('description')->value;
}
/**
* {@inheritdoc}
*/
public function setDescription($description) {
$this->set('description', $description);
return $this;
}
/**
* {@inheritdoc}
*/
public function getFormat() {
return $this->get('description')->format;
}
/**
* {@inheritdoc}
*/
public function setFormat($format) {
$this->get('description')->format = $format;
return $this;
}
/**
* {@inheritdoc}
*/
public function getName() {
return $this->label();
}
/**
* {@inheritdoc}
*/
public function setName($name) {
$this->set('name', $name);
return $this;
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->get('weight')->value;
}
/**
* {@inheritdoc}
*/
public function setWeight($weight) {
$this->set('weight', $weight);
return $this;
}
/**
* {@inheritdoc}
*/
public function getVocabularyId() {
@trigger_error('The ' . __METHOD__ . ' method is deprecated since version 8.4.0 and will be removed before 9.0.0. Use ' . __CLASS__ . '::bundle() instead to get the vocabulary ID.', E_USER_DEPRECATED);
return $this->bundle();
}
}
@@ -0,0 +1,178 @@
<?php
namespace Drupal\taxonomy\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\taxonomy\VocabularyInterface;
/**
* Defines the taxonomy vocabulary entity.
*
* @ConfigEntityType(
* id = "taxonomy_vocabulary",
* label = @Translation("Taxonomy vocabulary"),
* label_singular = @Translation("vocabulary"),
* label_plural = @Translation("vocabularies"),
* label_collection = @Translation("Taxonomy"),
* label_count = @PluralTranslation(
* singular = "@count vocabulary",
* plural = "@count vocabularies"
* ),
* handlers = {
* "storage" = "Drupal\taxonomy\VocabularyStorage",
* "list_builder" = "Drupal\taxonomy\VocabularyListBuilder",
* "access" = "Drupal\taxonomy\VocabularyAccessControlHandler",
* "form" = {
* "default" = "Drupal\taxonomy\VocabularyForm",
* "reset" = "Drupal\taxonomy\Form\VocabularyResetForm",
* "delete" = "Drupal\taxonomy\Form\VocabularyDeleteForm",
* "overview" = "Drupal\taxonomy\Form\OverviewTerms"
* },
* "route_provider" = {
* "html" = "Drupal\taxonomy\Entity\Routing\VocabularyRouteProvider",
* }
* },
* admin_permission = "administer taxonomy",
* config_prefix = "vocabulary",
* bundle_of = "taxonomy_term",
* entity_keys = {
* "id" = "vid",
* "label" = "name",
* "weight" = "weight"
* },
* links = {
* "add-form" = "/admin/structure/taxonomy/add",
* "delete-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/delete",
* "reset-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/reset",
* "overview-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/overview",
* "edit-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}",
* "collection" = "/admin/structure/taxonomy",
* },
* config_export = {
* "name",
* "vid",
* "description",
* "weight",
* }
* )
*/
class Vocabulary extends ConfigEntityBundleBase implements VocabularyInterface {
/**
* The taxonomy vocabulary ID.
*
* @var string
*/
protected $vid;
/**
* Name of the vocabulary.
*
* @var string
*/
protected $name;
/**
* Description of the vocabulary.
*
* @var string
*/
protected $description;
/**
* The weight of this vocabulary in relation to other vocabularies.
*
* @var int
*/
protected $weight = 0;
/**
* {@inheritdoc}
*/
public function getHierarchy() {
@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) {
@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;
}
/**
* {@inheritdoc}
*/
public function id() {
return $this->vid;
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description;
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);
// Only load terms without a parent, child terms will get deleted too.
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$terms = $term_storage->loadMultiple($storage->getToplevelTids(array_keys($entities)));
$term_storage->delete($terms);
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
// Reset caches.
$storage->resetCache(array_keys($entities));
if (reset($entities)->isSyncing()) {
return;
}
$vocabularies = [];
foreach ($entities as $vocabulary) {
$vocabularies[$vocabulary->id()] = $vocabulary->id();
}
// Load all Taxonomy module fields and delete those which use only this
// vocabulary.
$field_storages = \Drupal::entityTypeManager()->getStorage('field_storage_config')->loadByProperties(['module' => 'taxonomy']);
foreach ($field_storages as $field_storage) {
$modified_storage = FALSE;
// Term reference fields may reference terms from more than one
// vocabulary.
foreach ($field_storage->getSetting('allowed_values') as $key => $allowed_value) {
if (isset($vocabularies[$allowed_value['vocabulary']])) {
$allowed_values = $field_storage->getSetting('allowed_values');
unset($allowed_values[$key]);
$field_storage->setSetting('allowed_values', $allowed_values);
$modified_storage = TRUE;
}
}
if ($modified_storage) {
$allowed_values = $field_storage->getSetting('allowed_values');
if (empty($allowed_values)) {
$field_storage->delete();
}
else {
// Update the field definition with the new allowed values.
$field_storage->save();
}
}
}
}
}
@@ -0,0 +1,616 @@
<?php
namespace Drupal\taxonomy\Form;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Pager\PagerManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\taxonomy\VocabularyInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides terms overview form for a taxonomy vocabulary.
*
* @internal
*/
class OverviewTerms extends FormBase {
use DeprecatedServicePropertyTrait;
/**
* {@inheritdoc}
*/
protected $deprecatedProperties = ['entityManager' => 'entity.manager'];
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The term storage handler.
*
* @var \Drupal\taxonomy\TermStorageInterface
*/
protected $storageController;
/**
* The term list builder.
*
* @var \Drupal\Core\Entity\EntityListBuilderInterface
*/
protected $termListBuilder;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* The pager manager.
*
* @var \Drupal\Core\Pager\PagerManagerInterface
*/
protected $pagerManager;
/**
* Constructs an OverviewTerms object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param \Drupal\Core\Pager\PagerManagerInterface|null $pager_manager
* The pager manager.
*/
public function __construct(ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer = NULL, EntityRepositoryInterface $entity_repository = NULL, PagerManagerInterface $pager_manager = NULL) {
$this->moduleHandler = $module_handler;
$this->entityTypeManager = $entity_type_manager;
$this->storageController = $entity_type_manager->getStorage('taxonomy_term');
$this->termListBuilder = $entity_type_manager->getListBuilder('taxonomy_term');
$this->renderer = $renderer ?: \Drupal::service('renderer');
if (!$entity_repository) {
@trigger_error('Calling OverviewTerms::__construct() with the $entity_repository argument is supported in drupal:8.7.0 and will be required before drupal:9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
$entity_repository = \Drupal::service('entity.repository');
}
$this->entityRepository = $entity_repository;
if (!$pager_manager) {
@trigger_error('Calling OverviewTerms::__construct() without the $pager_manager argument is deprecated in drupal:8.8.0 and the $pager_manager argument will be required in drupal:9.0.0. See https://www.drupal.org/node/2779457', E_USER_DEPRECATED);
$pager_manager = \Drupal::service('pager.manager');
}
$this->pagerManager = $pager_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
$container->get('entity_type.manager'),
$container->get('renderer'),
$container->get('entity.repository'),
$container->get('pager.manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'taxonomy_overview_terms';
}
/**
* Form constructor.
*
* Display a tree of all the terms in a vocabulary, with options to edit
* each one. The form is made drag and drop by the theme function.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\taxonomy\VocabularyInterface $taxonomy_vocabulary
* The vocabulary to display the overview form for.
*
* @return array
* The form structure.
*/
public function buildForm(array $form, FormStateInterface $form_state, VocabularyInterface $taxonomy_vocabulary = NULL) {
$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;
// Number of terms per page.
$page_increment = $this->config('taxonomy.settings')->get('terms_per_page_admin');
// Elements shown on this page.
$page_entries = 0;
// Elements at the root level before this page.
$before_entries = 0;
// Elements at the root level after this page.
$after_entries = 0;
// Elements at the root level on this page.
$root_entries = 0;
// Terms from previous and next pages are shown if the term tree would have
// been cut in the middle. Keep track of how many extra terms we show on
// each page of terms.
$back_step = NULL;
$forward_step = 0;
// An array of the terms to be displayed on this page.
$current_page = [];
$delta = 0;
$term_deltas = [];
$tree = $this->storageController->loadTree($taxonomy_vocabulary->id(), 0, NULL, TRUE);
$tree_index = 0;
do {
// In case this tree is completely empty.
if (empty($tree[$tree_index])) {
break;
}
$delta++;
// Count entries before the current page.
if ($page && ($page * $page_increment) > $before_entries && !isset($back_step)) {
$before_entries++;
continue;
}
// Count entries after the current page.
elseif ($page_entries > $page_increment && isset($complete_tree)) {
$after_entries++;
continue;
}
// Do not let a term start the page that is not at the root.
$term = $tree[$tree_index];
if (isset($term->depth) && ($term->depth > 0) && !isset($back_step)) {
$back_step = 0;
while ($pterm = $tree[--$tree_index]) {
$before_entries--;
$back_step++;
if ($pterm->depth == 0) {
$tree_index--;
// Jump back to the start of the root level parent.
continue 2;
}
}
}
$back_step = isset($back_step) ? $back_step : 0;
// Continue rendering the tree until we reach the a new root item.
if ($page_entries >= $page_increment + $back_step + 1 && $term->depth == 0 && $root_entries > 1) {
$complete_tree = TRUE;
// This new item at the root level is the first item on the next page.
$after_entries++;
continue;
}
if ($page_entries >= $page_increment + $back_step) {
$forward_step++;
}
// Finally, if we've gotten down this far, we're rendering a term on this
// page.
$page_entries++;
$term_deltas[$term->id()] = isset($term_deltas[$term->id()]) ? $term_deltas[$term->id()] + 1 : 0;
$key = 'tid:' . $term->id() . ':' . $term_deltas[$term->id()];
// Keep track of the first term displayed on this page.
if ($page_entries == 1) {
$form['#first_tid'] = $term->id();
}
// Keep a variable to make sure at least 2 root elements are displayed.
if ($term->parents[0] == 0) {
$root_entries++;
}
$current_page[$key] = $term;
} while (isset($tree[++$tree_index]));
// Because we didn't use a pager query, set the necessary pager variables.
$total_entries = $before_entries + $page_entries + $after_entries;
$this->pagerManager->createPager($total_entries, $page_increment);
// If this form was already submitted once, it's probably hit a validation
// error. Ensure the form is rebuilt in the same order as the user
// submitted.
$user_input = $form_state->getUserInput();
if (!empty($user_input)) {
// Get the POST order.
$order = array_flip(array_keys($user_input['terms']));
// Update our form with the new order.
$current_page = array_merge($order, $current_page);
foreach ($current_page as $key => $term) {
// Verify this is a term for the current page and set at the current
// depth.
if (is_array($user_input['terms'][$key]) && is_numeric($user_input['terms'][$key]['term']['tid'])) {
$current_page[$key]->depth = $user_input['terms'][$key]['term']['depth'];
}
else {
unset($current_page[$key]);
}
}
}
$args = [
'%capital_name' => Unicode::ucfirst($taxonomy_vocabulary->label()),
'%name' => $taxonomy_vocabulary->label(),
];
if ($this->currentUser()->hasPermission('administer taxonomy') || $this->currentUser()->hasPermission('edit terms in ' . $taxonomy_vocabulary->id())) {
switch ($vocabulary_hierarchy) {
case VocabularyInterface::HIERARCHY_DISABLED:
$help_message = $this->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.', $args);
break;
case VocabularyInterface::HIERARCHY_SINGLE:
$help_message = $this->t('%capital_name contains terms grouped under parent terms. You can reorganize the terms in %capital_name using their drag-and-drop handles.', $args);
break;
case VocabularyInterface::HIERARCHY_MULTIPLE:
$help_message = $this->t('%capital_name contains terms with multiple parents. Drag and drop of terms with multiple parents is not supported, but you can re-enable drag-and-drop support by editing each term to include only a single parent.', $args);
break;
}
}
else {
switch ($vocabulary_hierarchy) {
case VocabularyInterface::HIERARCHY_DISABLED:
$help_message = $this->t('%capital_name contains the following terms.', $args);
break;
case VocabularyInterface::HIERARCHY_SINGLE:
$help_message = $this->t('%capital_name contains terms grouped under parent terms', $args);
break;
case VocabularyInterface::HIERARCHY_MULTIPLE:
$help_message = $this->t('%capital_name contains terms with multiple parents.', $args);
break;
}
}
// Get the IDs of the terms edited on the current page which have pending
// revisions.
$edited_term_ids = array_map(function ($item) {
return $item->id();
}, $current_page);
$pending_term_ids = array_intersect($this->storageController->getTermIdsWithPendingRevisions(), $edited_term_ids);
if ($pending_term_ids) {
$help_message = $this->formatPlural(
count($pending_term_ids),
'%capital_name contains 1 term with pending revisions. Drag and drop of terms with pending revisions is not supported, but you can re-enable drag-and-drop support by getting each term to a published state.',
'%capital_name contains @count terms with pending revisions. Drag and drop of terms with pending revisions is not supported, but you can re-enable drag-and-drop support by getting each term to a published state.',
$args
);
}
// Only allow access to change parents and reorder the tree if there are no
// pending revisions and there are no terms with multiple parents.
$update_tree_access = AccessResult::allowedIf(empty($pending_term_ids) && $vocabulary_hierarchy !== VocabularyInterface::HIERARCHY_MULTIPLE);
$form['help'] = [
'#type' => 'container',
'message' => ['#markup' => $help_message],
];
if (!$update_tree_access->isAllowed()) {
$form['help']['#attributes']['class'] = ['messages', 'messages--warning'];
}
$errors = $form_state->getErrors();
$row_position = 0;
// Build the actual form.
$access_control_handler = $this->entityTypeManager->getAccessControlHandler('taxonomy_term');
$create_access = $access_control_handler->createAccess($taxonomy_vocabulary->id(), NULL, [], TRUE);
if ($create_access->isAllowed()) {
$empty = $this->t('No terms available. <a href=":link">Add term</a>.', [':link' => Url::fromRoute('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $taxonomy_vocabulary->id()])->toString()]);
}
else {
$empty = $this->t('No terms available.');
}
$form['terms'] = [
'#type' => 'table',
'#empty' => $empty,
'#header' => [
'term' => $this->t('Name'),
'operations' => $this->t('Operations'),
'weight' => $update_tree_access->isAllowed() ? $this->t('Weight') : NULL,
],
'#attributes' => [
'id' => 'taxonomy',
],
];
$this->renderer->addCacheableDependency($form['terms'], $create_access);
foreach ($current_page as $key => $term) {
$form['terms'][$key] = [
'term' => [],
'operations' => [],
'weight' => $update_tree_access->isAllowed() ? [] : NULL,
];
/** @var $term \Drupal\Core\Entity\EntityInterface */
$term = $this->entityRepository->getTranslationFromContext($term);
$form['terms'][$key]['#term'] = $term;
$indentation = [];
if (isset($term->depth) && $term->depth > 0) {
$indentation = [
'#theme' => 'indentation',
'#size' => $term->depth,
];
}
$form['terms'][$key]['term'] = [
'#prefix' => !empty($indentation) ? $this->renderer->render($indentation) : '',
'#type' => 'link',
'#title' => $term->getName(),
'#url' => $term->toUrl(),
];
// Add a special class for terms with pending revision so we can highlight
// them in the form.
$form['terms'][$key]['#attributes']['class'] = [];
if (in_array($term->id(), $pending_term_ids)) {
$form['terms'][$key]['#attributes']['class'][] = 'color-warning';
$form['terms'][$key]['#attributes']['class'][] = 'taxonomy-term--pending-revision';
}
if ($update_tree_access->isAllowed() && count($tree) > 1) {
$parent_fields = TRUE;
$form['terms'][$key]['term']['tid'] = [
'#type' => 'hidden',
'#value' => $term->id(),
'#attributes' => [
'class' => ['term-id'],
],
];
$form['terms'][$key]['term']['parent'] = [
'#type' => 'hidden',
// Yes, default_value on a hidden. It needs to be changeable by the
// javascript.
'#default_value' => $term->parents[0],
'#attributes' => [
'class' => ['term-parent'],
],
];
$form['terms'][$key]['term']['depth'] = [
'#type' => 'hidden',
// Same as above, the depth is modified by javascript, so it's a
// default_value.
'#default_value' => $term->depth,
'#attributes' => [
'class' => ['term-depth'],
],
];
}
$update_access = $term->access('update', NULL, TRUE);
$update_tree_access = $update_tree_access->andIf($update_access);
if ($update_tree_access->isAllowed()) {
$form['terms'][$key]['weight'] = [
'#type' => 'weight',
'#delta' => $delta,
'#title' => $this->t('Weight for added term'),
'#title_display' => 'invisible',
'#default_value' => $term->getWeight(),
'#attributes' => ['class' => ['term-weight']],
];
}
if ($operations = $this->termListBuilder->getOperations($term)) {
$form['terms'][$key]['operations'] = [
'#type' => 'operations',
'#links' => $operations,
];
}
if ($parent_fields) {
$form['terms'][$key]['#attributes']['class'][] = 'draggable';
}
// Add classes that mark which terms belong to previous and next pages.
if ($row_position < $back_step || $row_position >= $page_entries - $forward_step) {
$form['terms'][$key]['#attributes']['class'][] = 'taxonomy-term-preview';
}
if ($row_position !== 0 && $row_position !== count($tree) - 1) {
if ($row_position == $back_step - 1 || $row_position == $page_entries - $forward_step - 1) {
$form['terms'][$key]['#attributes']['class'][] = 'taxonomy-term-divider-top';
}
elseif ($row_position == $back_step || $row_position == $page_entries - $forward_step) {
$form['terms'][$key]['#attributes']['class'][] = 'taxonomy-term-divider-bottom';
}
}
// Add an error class if this row contains a form error.
foreach ($errors as $error_key => $error) {
if (strpos($error_key, $key) === 0) {
$form['terms'][$key]['#attributes']['class'][] = 'error';
}
}
$row_position++;
}
$this->renderer->addCacheableDependency($form['terms'], $update_tree_access);
if ($update_tree_access->isAllowed()) {
if ($parent_fields) {
$form['terms']['#tabledrag'][] = [
'action' => 'match',
'relationship' => 'parent',
'group' => 'term-parent',
'subgroup' => 'term-parent',
'source' => 'term-id',
'hidden' => FALSE,
];
$form['terms']['#tabledrag'][] = [
'action' => 'depth',
'relationship' => 'group',
'group' => 'term-depth',
'hidden' => FALSE,
];
$form['terms']['#attached']['library'][] = 'taxonomy/drupal.taxonomy';
$form['terms']['#attached']['drupalSettings']['taxonomy'] = [
'backStep' => $back_step,
'forwardStep' => $forward_step,
];
}
$form['terms']['#tabledrag'][] = [
'action' => 'order',
'relationship' => 'sibling',
'group' => 'term-weight',
];
}
if ($update_tree_access->isAllowed() && count($tree) > 1) {
$form['actions'] = ['#type' => 'actions', '#tree' => FALSE];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#button_type' => 'primary',
];
$form['actions']['reset_alphabetical'] = [
'#type' => 'submit',
'#submit' => ['::submitReset'],
'#value' => $this->t('Reset to alphabetical'),
];
}
$form['pager_pager'] = ['#type' => 'pager'];
return $form;
}
/**
* Form submission handler.
*
* Rather than using a textfield or weight field, this form depends entirely
* upon the order of form elements on the page to determine new weights.
*
* Because there might be hundreds or thousands of taxonomy terms that need to
* be ordered, terms are weighted from 0 to the number of terms in the
* vocabulary, rather than the standard -10 to 10 scale. Numbers are sorted
* lowest to highest, but are not necessarily sequential. Numbers may be
* skipped when a term has children so that reordering is minimal when a child
* is added or removed from a term.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Sort term order based on weight.
uasort($form_state->getValue('terms'), ['Drupal\Component\Utility\SortArray', 'sortByWeightElement']);
$vocabulary = $form_state->get(['taxonomy', 'vocabulary']);
$changed_terms = [];
$tree = $this->storageController->loadTree($vocabulary->id(), 0, NULL, TRUE);
if (empty($tree)) {
return;
}
// Build a list of all terms that need to be updated on previous pages.
$weight = 0;
$term = $tree[0];
while ($term->id() != $form['#first_tid']) {
if ($term->parents[0] == 0 && $term->getWeight() != $weight) {
$term->setWeight($weight);
$changed_terms[$term->id()] = $term;
}
$weight++;
$term = $tree[$weight];
}
// Renumber the current page weights and assign any new parents.
$level_weights = [];
foreach ($form_state->getValue('terms') as $tid => $values) {
if (isset($form['terms'][$tid]['#term'])) {
$term = $form['terms'][$tid]['#term'];
// Give terms at the root level a weight in sequence with terms on previous pages.
if ($values['term']['parent'] == 0 && $term->getWeight() != $weight) {
$term->setWeight($weight);
$changed_terms[$term->id()] = $term;
}
// Terms not at the root level can safely start from 0 because they're all on this page.
elseif ($values['term']['parent'] > 0) {
$level_weights[$values['term']['parent']] = isset($level_weights[$values['term']['parent']]) ? $level_weights[$values['term']['parent']] + 1 : 0;
if ($level_weights[$values['term']['parent']] != $term->getWeight()) {
$term->setWeight($level_weights[$values['term']['parent']]);
$changed_terms[$term->id()] = $term;
}
}
// Update any changed parents.
if ($values['term']['parent'] != $term->parents[0]) {
$term->parent->target_id = $values['term']['parent'];
$changed_terms[$term->id()] = $term;
}
$weight++;
}
}
// Build a list of all terms that need to be updated on following pages.
for ($weight; $weight < count($tree); $weight++) {
$term = $tree[$weight];
if ($term->parents[0] == 0 && $term->getWeight() != $weight) {
$term->parent->target_id = $term->parents[0];
$term->setWeight($weight);
$changed_terms[$term->id()] = $term;
}
}
if (!empty($changed_terms)) {
$pending_term_ids = $this->storageController->getTermIdsWithPendingRevisions();
// Force a form rebuild if any of the changed terms has a pending
// revision.
if (array_intersect_key(array_flip($pending_term_ids), $changed_terms)) {
$this->messenger()->addError($this->t('The terms with updated parents have been modified by another user, the changes could not be saved.'));
$form_state->setRebuild();
return;
}
// Save all updated terms.
foreach ($changed_terms as $term) {
$term->save();
}
$this->messenger()->addStatus($this->t('The configuration options have been saved.'));
}
}
/**
* Redirects to confirmation form for the reset action.
*/
public function submitReset(array &$form, FormStateInterface $form_state) {
/** @var $vocabulary \Drupal\taxonomy\VocabularyInterface */
$vocabulary = $form_state->get(['taxonomy', 'vocabulary']);
$form_state->setRedirectUrl($vocabulary->toUrl('reset-form'));
}
}
@@ -0,0 +1,45 @@
<?php
namespace Drupal\taxonomy\Form;
use Drupal\Core\Entity\ContentEntityDeleteForm;
use Drupal\Core\Url;
/**
* Provides a deletion confirmation form for taxonomy term.
*
* @internal
*/
class TermDeleteForm extends ContentEntityDeleteForm {
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
// The cancel URL is the vocabulary collection, terms have no global
// list page.
return new Url('entity.taxonomy_vocabulary.collection');
}
/**
* {@inheritdoc}
*/
protected function getRedirectUrl() {
return $this->getCancelUrl();
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Deleting a term will delete all its children if there are any. This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
protected function getDeletionMessage() {
return $this->t('Deleted term %name.', ['%name' => $this->entity->label()]);
}
}
@@ -0,0 +1,42 @@
<?php
namespace Drupal\taxonomy\Form;
use Drupal\Core\Entity\EntityDeleteForm;
/**
* Provides a deletion confirmation form for taxonomy vocabulary.
*
* @internal
*/
class VocabularyDeleteForm extends EntityDeleteForm {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'taxonomy_vocabulary_confirm_delete';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete the vocabulary %title?', ['%title' => $this->entity->label()]);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Deleting a vocabulary will delete all the terms in it. This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
protected function getDeletionMessage() {
return $this->t('Deleted vocabulary %name.', ['%name' => $this->entity->label()]);
}
}
@@ -0,0 +1,90 @@
<?php
namespace Drupal\taxonomy\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\taxonomy\TermStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides confirmation form for resetting a vocabulary to alphabetical order.
*
* @internal
*/
class VocabularyResetForm extends EntityConfirmFormBase {
/**
* The term storage.
*
* @var \Drupal\taxonomy\TermStorageInterface
*/
protected $termStorage;
/**
* Constructs a new VocabularyResetForm object.
*
* @param \Drupal\taxonomy\TermStorageInterface $term_storage
* The term storage.
*/
public function __construct(TermStorageInterface $term_storage) {
$this->termStorage = $term_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('taxonomy_term')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'taxonomy_vocabulary_confirm_reset_alphabetical';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to reset the vocabulary %title to alphabetical order?', ['%title' => $this->entity->label()]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->entity->toUrl('overview-form');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Resetting a vocabulary will discard all custom ordering and sort items alphabetically.');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Reset to alphabetical');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$this->termStorage->resetWeights($this->entity->id());
$this->messenger()->addStatus($this->t('Reset vocabulary %name to alphabetical order.', ['%name' => $this->entity->label()]));
$this->logger('taxonomy')->notice('Reset vocabulary %name to alphabetical order.', ['%name' => $this->entity->label()]);
$form_state->setRedirectUrl($this->getCancelUrl());
}
}
@@ -0,0 +1,141 @@
<?php
namespace Drupal\taxonomy\Plugin\EntityReferenceSelection;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\Core\Form\FormStateInterface;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Provides specific access control for the taxonomy_term entity type.
*
* @EntityReferenceSelection(
* id = "default:taxonomy_term",
* label = @Translation("Taxonomy Term selection"),
* entity_types = {"taxonomy_term"},
* group = "default",
* weight = 1
* )
*/
class TermSelection extends DefaultSelection {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'sort' => [
'field' => 'name',
'direction' => 'asc',
],
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
// Sorting is not possible for taxonomy terms because we use
// \Drupal\taxonomy\TermStorageInterface::loadTree() to retrieve matches.
$form['sort']['#access'] = FALSE;
return $form;
}
/**
* {@inheritdoc}
*/
public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
if ($match || $limit) {
return parent::getReferenceableEntities($match, $match_operator, $limit);
}
$options = [];
$bundles = $this->entityTypeBundleInfo->getBundleInfo('taxonomy_term');
$bundle_names = $this->getConfiguration()['target_bundles'] ?: array_keys($bundles);
$has_admin_access = $this->currentUser->hasPermission('administer taxonomy');
$unpublished_terms = [];
foreach ($bundle_names as $bundle) {
if ($vocabulary = Vocabulary::load($bundle)) {
/** @var \Drupal\taxonomy\TermInterface[] $terms */
if ($terms = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vocabulary->id(), 0, NULL, TRUE)) {
foreach ($terms as $term) {
if (!$has_admin_access && (!$term->isPublished() || in_array($term->parent->target_id, $unpublished_terms))) {
$unpublished_terms[] = $term->id();
continue;
}
$options[$vocabulary->id()][$term->id()] = str_repeat('-', $term->depth) . Html::escape($this->entityRepository->getTranslationFromContext($term)->label());
}
}
}
}
return $options;
}
/**
* {@inheritdoc}
*/
public function countReferenceableEntities($match = NULL, $match_operator = 'CONTAINS') {
if ($match) {
return parent::countReferenceableEntities($match, $match_operator);
}
$total = 0;
$referenceable_entities = $this->getReferenceableEntities($match, $match_operator, 0);
foreach ($referenceable_entities as $bundle => $entities) {
$total += count($entities);
}
return $total;
}
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
// Adding the 'taxonomy_term_access' tag is sadly insufficient for terms:
// core requires us to also know about the concept of 'published' and
// 'unpublished'.
if (!$this->currentUser->hasPermission('administer taxonomy')) {
$query->condition('status', 1);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$term = parent::createNewEntity($entity_type_id, $bundle, $label, $uid);
// In order to create a referenceable term, it needs to published.
/** @var \Drupal\taxonomy\TermInterface $term */
$term->setPublished();
return $term;
}
/**
* {@inheritdoc}
*/
public function validateReferenceableNewEntities(array $entities) {
$entities = parent::validateReferenceableNewEntities($entities);
// Mirror the conditions checked in buildEntityQuery().
if (!$this->currentUser->hasPermission('administer taxonomy')) {
$entities = array_filter($entities, function ($term) {
/** @var \Drupal\taxonomy\TermInterface $term */
return $term->isPublished();
});
}
return $entities;
}
}
@@ -0,0 +1,52 @@
<?php
namespace Drupal\taxonomy\Plugin\Field\FieldFormatter;
use Drupal\Core\Url;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase;
/**
* Plugin implementation of the 'entity reference taxonomy term RSS' formatter.
*
* @FieldFormatter(
* id = "entity_reference_rss_category",
* label = @Translation("RSS category"),
* description = @Translation("Display reference to taxonomy term in RSS."),
* field_types = {
* "entity_reference"
* }
* )
*/
class EntityReferenceTaxonomyTermRssFormatter extends EntityReferenceFormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$parent_entity = $items->getEntity();
$elements = [];
foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) {
$parent_entity->rss_elements[] = [
'key' => 'category',
'value' => $entity->label(),
'attributes' => [
'domain' => $entity->id() ? Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $entity->id()], ['absolute' => TRUE])->toString() : '',
],
];
}
return $elements;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
// This formatter is only available for taxonomy terms.
return $field_definition->getFieldStorageDefinition()->getSetting('target_type') == 'taxonomy_term';
}
}
@@ -0,0 +1,31 @@
<?php
namespace Drupal\taxonomy\Plugin\Validation\Constraint;
use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;
/**
* Validation constraint for changing the term hierarchy in pending revisions.
*
* @Constraint(
* id = "TaxonomyHierarchy",
* label = @Translation("Taxonomy term hierarchy.", context = "Validation"),
* )
*/
class TaxonomyTermHierarchyConstraint extends CompositeConstraintBase {
/**
* The default violation message.
*
* @var string
*/
public $message = 'You can only change the hierarchy for the <em>published</em> version of this term.';
/**
* {@inheritdoc}
*/
public function coversFields() {
return ['parent', 'weight'];
}
}
@@ -0,0 +1,77 @@
<?php
namespace Drupal\taxonomy\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\taxonomy\TermStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Constraint validator for changing term parents in pending revisions.
*/
class TaxonomyTermHierarchyConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
private $entityTypeManager;
/**
* Creates a new TaxonomyTermHierarchyConstraintValidator instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function validate($entity, Constraint $constraint) {
$term_storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
assert($term_storage instanceof TermStorageInterface);
// Newly created entities should be able to specify a parent.
if ($entity && $entity->isNew()) {
return;
}
$is_pending_revision = !$entity->isDefaultRevision();
$pending_term_ids = $term_storage->getTermIdsWithPendingRevisions();
$ancestors = $term_storage->loadAllParents($entity->id());
$ancestor_is_pending_revision = (bool) array_intersect_key($ancestors, array_flip($pending_term_ids));
$new_parents = array_column($entity->parent->getValue(), 'target_id');
$original_parents = array_keys($term_storage->loadParents($entity->id())) ?: [0];
if (($is_pending_revision || $ancestor_is_pending_revision) && $new_parents != $original_parents) {
$a = 1;
$this->context->buildViolation($constraint->message)
->atPath('parent')
->addViolation();
}
$original = $term_storage->loadUnchanged($entity->id());
if (($is_pending_revision || $ancestor_is_pending_revision) && !$entity->weight->equals($original->weight)) {
$this->context->buildViolation($constraint->message)
->atPath('weight')
->addViolation();
}
}
}
@@ -0,0 +1,73 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\migrate\Plugin\MigrationDeriverTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Deriver for Drupal 6 term node migrations based on vocabularies.
*/
class D6TermNodeDeriver extends DeriverBase implements ContainerDeriverInterface {
use MigrationDeriverTrait;
/**
* The base plugin ID this derivative is for.
*
* @var string
*/
protected $basePluginId;
/**
* The migration plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $migrationPluginManager;
/**
* D6TermNodeDeriver constructor.
*
* @param string $base_plugin_id
* The base plugin ID this derivative is for.
* @param \Drupal\Component\Plugin\PluginManagerInterface $migration_plugin_manager
* The migration plugin manager.
*/
public function __construct($base_plugin_id, PluginManagerInterface $migration_plugin_manager) {
$this->basePluginId = $base_plugin_id;
$this->migrationPluginManager = $migration_plugin_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$base_plugin_id,
$container->get('plugin.manager.migration')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition, $base_plugin_definitions = NULL) {
try {
foreach (static::getSourcePlugin('d6_taxonomy_vocabulary') as $row) {
$source_vid = $row->getSourceProperty('vid');
$definition = $base_plugin_definition;
$definition['source']['vid'] = $source_vid;
// migrate_drupal_migration_plugins_alter() adds to this definition.
$this->derivatives[$source_vid] = $definition;
}
}
catch (\Exception $e) {
// It is possible no D6 tables are loaded so just eat exceptions.
}
return $this->derivatives;
}
}
@@ -0,0 +1,100 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrationDeriverTrait;
use Drupal\migrate_drupal\FieldDiscoveryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Deriver for Drupal 7 taxonomy term migrations based on vocabularies.
*/
class D7TaxonomyTermDeriver extends DeriverBase implements ContainerDeriverInterface {
use MigrationDeriverTrait;
/**
* The base plugin ID this derivative is for.
*
* @var string
*/
protected $basePluginId;
/**
* The migration field discovery service.
*
* @var \Drupal\migrate_drupal\FieldDiscoveryInterface
*/
protected $fieldDiscovery;
/**
* D7TaxonomyTermDeriver constructor.
*
* @param string $base_plugin_id
* The base plugin ID for the plugin ID.
* @param \Drupal\migrate_drupal\FieldDiscoveryInterface $field_discovery
* The migration field discovery service.
*/
public function __construct($base_plugin_id, FieldDiscoveryInterface $field_discovery) {
$this->basePluginId = $base_plugin_id;
$this->fieldDiscovery = $field_discovery;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$base_plugin_id,
$container->get('migrate_drupal.field_discovery')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$vocabulary_source_plugin = static::getSourcePlugin('d7_taxonomy_vocabulary');
try {
$vocabulary_source_plugin->checkRequirements();
}
catch (RequirementsException $e) {
// If the d7_taxonomy_vocabulary requirements failed, that means we do not
// have a Drupal source database configured - there is nothing to
// generate.
return $this->derivatives;
}
try {
foreach ($vocabulary_source_plugin as $row) {
$bundle = $row->getSourceProperty('machine_name');
$values = $base_plugin_definition;
$values['label'] = t('@label (@type)', [
'@label' => $values['label'],
'@type' => $row->getSourceProperty('name'),
]);
$values['source']['bundle'] = $bundle;
$values['destination']['default_bundle'] = $bundle;
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($values);
$this->fieldDiscovery->addBundleFieldProcesses($migration, 'taxonomy_term', $bundle);
$this->derivatives[$bundle] = $migration->getPluginDefinition();
}
}
catch (DatabaseExceptionWrapper $e) {
// Once we begin iterating the source plugin it is possible that the
// source tables will not exist. This can happen when the
// MigrationPluginManager gathers up the migration definitions but we do
// not actually have a Drupal 7 source database.
}
return $this->derivatives;
}
}
@@ -0,0 +1,42 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\cckfield;
@trigger_error('TaxonomyTermReference is deprecated in Drupal 8.4.x and will be removed before Drupal 9.0.x. Use \Drupal\taxonomy\Plugin\migrate\field\TaxonomyTermReference instead.', E_USER_DEPRECATED);
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\Plugin\migrate\cckfield\CckFieldPluginBase;
/**
* @MigrateCckField(
* id = "taxonomy_term_reference",
* type_map = {
* "taxonomy_term_reference" = "entity_reference"
* },
* core = {6,7},
* source_module = "taxonomy",
* destination_module = "core",
* )
*
* @deprecated in drupal:8.4.0 and is removed from drupal:9.0.0. Use
* \Drupal\taxonomy\Plugin\migrate\field\TaxonomyTermReference instead.
*
* @see https://www.drupal.org/node/2751897
*/
class TaxonomyTermReference extends CckFieldPluginBase {
/**
* {@inheritdoc}
*/
public function processCckFieldValues(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'sub_process',
'source' => $field_name,
'process' => [
'target_id' => 'tid',
],
];
$migration->setProcessOfProperty($field_name, $process);
}
}
@@ -0,0 +1,46 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\field;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
/**
* @MigrateField(
* id = "taxonomy_term_reference",
* type_map = {
* "taxonomy_term_reference" = "entity_reference"
* },
* core = {6,7},
* source_module = "taxonomy",
* destination_module = "core",
* )
*/
class TaxonomyTermReference extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function getFieldFormatterMap() {
return [
'taxonomy_term_reference_link' => 'entity_reference_label',
'i18n_taxonomy_term_reference_link' => 'entity_reference_label',
'entityreference_entity_view' => 'entity_reference_entity_view',
];
}
/**
* {@inheritdoc}
*/
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'sub_process',
'source' => $field_name,
'process' => [
'target_id' => 'tid',
],
];
$migration->setProcessOfProperty($field_name, $process);
}
}
@@ -0,0 +1,46 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\process;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Checks if the vocabulary being migrated is the one used for forums.
*
* Drupal 8 Forum is expecting specific machine names for its field and
* vocabulary names. This process plugin forces a given machine name to the
* field or vocabulary that is being migrated.
*
* The 'forum_vocabulary' source property is evaluated in the
* d6_taxonomy_vocabulary or d7_taxonomy_vocabulary source plugins and is set to
* true if the vocabulary vid being migrated is the same as the one in the
* 'forum_nav_vocabulary' variable on the source site.
*
* Example:
*
* @code
* process:
* field_name:
* plugin: forum_vocabulary
* machine_name: taxonomy_forums
* @endcode
*
* @MigrateProcessPlugin(
* id = "forum_vocabulary"
* )
*/
class ForumVocabulary extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if ($row->getSourceProperty('forum_vocabulary') && !empty($this->configuration['machine_name'])) {
$value = $this->configuration['machine_name'];
}
return $value;
}
}
@@ -0,0 +1,106 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Taxonomy term source from database.
*
* @todo Support term_relation, term_synonym table if possible.
*
* @MigrateSource(
* id = "taxonomy_term",
* source_module = "taxonomy"
* )
*
* @deprecated in drupal:8.3.0 and is removed from drupal:9.0.0.
* Use \Drupal\taxonomy\Plugin\migrate\source\d6\Term or
* \Drupal\taxonomy\Plugin\migrate\source\d7\Term.
*
* @see https://www.drupal.org/node/2879193
*/
class Term extends DrupalSqlBase {
/**
* Name of the term data table.
*
* @var string
*/
protected $termDataTable;
/**
* Name of the term hierarchy table.
*
* @var string
*/
protected $termHierarchyTable;
/**
* {@inheritdoc}
*/
public function query() {
if ($this->getModuleSchemaVersion('taxonomy') >= 7000) {
$this->termDataTable = 'taxonomy_term_data';
$this->termHierarchyTable = 'taxonomy_term_hierarchy';
}
else {
$this->termDataTable = 'term_data';
$this->termHierarchyTable = 'term_hierarchy';
}
$query = $this->select($this->termDataTable, 'td')
->fields('td')
->distinct()
->orderBy('td.tid');
if (isset($this->configuration['vocabulary'])) {
$query->condition('td.vid', (array) $this->configuration['vocabulary'], 'IN');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'tid' => $this->t('The term ID.'),
'vid' => $this->t('Existing term VID'),
'name' => $this->t('The name of the term.'),
'description' => $this->t('The term description.'),
'weight' => $this->t('Weight'),
'parent' => $this->t("The Drupal term IDs of the term's parents."),
];
if ($this->getModuleSchemaVersion('taxonomy') >= 7000) {
$fields['format'] = $this->t('Format of the term description.');
}
return $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Find parents for this row.
$parents = $this->select($this->termHierarchyTable, 'th')
->fields('th', ['parent', 'tid'])
->condition('tid', $row->getSourceProperty('tid'))
->execute()
->fetchCol();
$row->setSourceProperty('parent', $parents);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['tid']['type'] = 'integer';
return $ids;
}
}
@@ -0,0 +1,78 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Taxonomy term source from database.
*
* @todo Support term_relation, term_synonym table if possible.
*
* @MigrateSource(
* id = "d6_taxonomy_term",
* source_module = "taxonomy"
* )
*/
class Term extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('term_data', 'td')
->fields('td')
->distinct()
->orderBy('td.tid');
if (isset($this->configuration['bundle'])) {
$query->condition('td.vid', (array) $this->configuration['bundle'], 'IN');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'tid' => $this->t('The term ID.'),
'vid' => $this->t('Existing term VID'),
'name' => $this->t('The name of the term.'),
'description' => $this->t('The term description.'),
'weight' => $this->t('Weight'),
'parent' => $this->t("The Drupal term IDs of the term's parents."),
];
if (isset($this->configuration['translations'])) {
$fields['language'] = $this->t('The term language.');
$fields['trid'] = $this->t('Translation ID.');
}
return $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Find parents for this row.
$parents = $this->select('term_hierarchy', 'th')
->fields('th', ['parent', 'tid'])
->condition('tid', $row->getSourceProperty('tid'))
->execute()
->fetchCol();
$row->setSourceProperty('parent', $parents);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['tid']['type'] = 'integer';
return $ids;
}
}
@@ -0,0 +1,105 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
/**
* Gets i18n taxonomy terms from source database.
*
* @MigrateSource(
* id = "d6_term_localized_translation",
* source_module = "i18ntaxonomy"
* )
*/
class TermLocalizedTranslation extends Term {
/**
* {@inheritdoc}
*/
public function query() {
// Ideally, the query would return rows for each language for each taxonomy
// term with the translations for both the name and description or just the
// name translation or just the description translation. That query quickly
// became complex and would be difficult to maintain.
// Therefore, build a query based on i18nstrings table where each row has
// the translation for only one property, either name or description. The
// method prepareRow() is then used to obtain the translation for the other
// property.
$query = parent::query();
$query->addField('td', 'language', 'td.language');
// Add in the property, which is either name or description.
// Cast td.tid as char for PostgreSQL compatibility.
$query->leftJoin('i18n_strings', 'i18n', 'CAST(td.tid AS CHAR(255)) = i18n.objectid');
$query->addField('i18n', 'lid');
$query->addField('i18n', 'property');
// Add in the translation for the property.
$query->innerJoin('locales_target', 'lt', 'i18n.lid = lt.lid');
$query->addField('lt', 'language', 'lt.language');
$query->addField('lt', 'translation');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$language = $row->getSourceProperty('ltlanguage');
$row->setSourceProperty('language', $language);
$tid = $row->getSourceProperty('tid');
// If this row has been migrated it is a duplicate then skip it.
if ($this->idMap->lookupDestinationIds(['tid' => $tid, 'language' => $language])) {
return FALSE;
}
// Save the translation for this property.
$property = $row->getSourceProperty('property');
$row->setSourceProperty($property . '_translated', $row->getSourceProperty('translation'));
// Get the translation, if one exists, for the property not already in the
// row.
$other_property = ($property == 'name') ? 'description' : 'name';
$query = $this->select('i18n_strings', 'i18n')
->fields('i18n', ['lid'])
->condition('i18n.property', $other_property)
->condition('i18n.objectid', $tid);
$query->leftJoin('locales_target', 'lt', 'i18n.lid = lt.lid');
$query->condition('lt.language', $language);
$query->addField('lt', 'translation');
$results = $query->execute()->fetchAssoc();
if ($results) {
$row->setSourceProperty($other_property . '_translated', $results['translation']);
}
else {
// The translation does not exist.
$row->setSourceProperty($other_property . '_translated', NULL);
}
parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'language' => $this->t('Language for this term.'),
'name_translated' => $this->t('Term name translation.'),
'description_translated' => $this->t('Term description translation.'),
];
return parent::fields() + $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['language']['type'] = 'string';
$ids['language']['alias'] = 'lt';
return parent::getIds() + $ids;
}
}
@@ -0,0 +1,71 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Source returning tids from the term_node table for the current revision.
*
* @MigrateSource(
* id = "d6_term_node",
* source_module = "taxonomy"
* )
*/
class TermNode extends DrupalSqlBase {
/**
* The join options between the node and the term node table.
*/
const JOIN = 'tn.vid = n.vid';
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('term_node', 'tn')
->distinct()
->fields('tn', ['nid', 'vid'])
->fields('n', ['type']);
// Because this is an inner join it enforces the current revision.
$query->innerJoin('term_data', 'td', 'td.tid = tn.tid AND td.vid = :vid', [':vid' => $this->configuration['vid']]);
$query->innerJoin('node', 'n', static::JOIN);
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'nid' => $this->t('The node revision ID.'),
'vid' => $this->t('The node revision ID.'),
'tid' => $this->t('The term ID.'),
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Select the terms belonging to the revision selected.
$query = $this->select('term_node', 'tn')
->fields('tn', ['tid'])
->condition('n.nid', $row->getSourceProperty('nid'));
$query->join('node', 'n', static::JOIN);
$query->innerJoin('term_data', 'td', 'td.tid = tn.tid AND td.vid = :vid', [':vid' => $this->configuration['vid']]);
$row->setSourceProperty('tid', $query->execute()->fetchCol());
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['vid']['type'] = 'integer';
$ids['vid']['alias'] = 'tn';
return $ids;
}
}
@@ -0,0 +1,20 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
/**
* Source returning tids from the term_node table for the non-current revision.
*
* @MigrateSource(
* id = "d6_term_node_revision",
* source_module = "taxonomy"
* )
*/
class TermNodeRevision extends TermNode {
/**
* {@inheritdoc}
*/
const JOIN = 'tn.nid = n.nid AND tn.vid != n.vid';
}
@@ -0,0 +1,91 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Drupal 6 vocabularies source from database.
*
* @MigrateSource(
* id = "d6_taxonomy_vocabulary",
* source_module = "taxonomy"
* )
*/
class Vocabulary extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('vocabulary', 'v')
->fields('v', [
'vid',
'name',
'description',
'help',
'relations',
'hierarchy',
'multiple',
'required',
'tags',
'module',
'weight',
]);
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'vid' => $this->t('The vocabulary ID.'),
'name' => $this->t('The name of the vocabulary.'),
'description' => $this->t('The description of the vocabulary.'),
'help' => $this->t('Help text to display for the vocabulary.'),
'relations' => $this->t('Whether or not related terms are enabled within the vocabulary. (0 = disabled, 1 = enabled)'),
'hierarchy' => $this->t('The type of hierarchy allowed within the vocabulary. (0 = disabled, 1 = single, 2 = multiple)'),
'multiple' => $this->t('Whether or not multiple terms from this vocabulary may be assigned to a node. (0 = disabled, 1 = enabled)'),
'required' => $this->t('Whether or not terms are required for nodes using this vocabulary. (0 = disabled, 1 = enabled)'),
'tags' => $this->t('Whether or not free tagging is enabled for the vocabulary. (0 = disabled, 1 = enabled)'),
'weight' => $this->t('The weight of the vocabulary in relation to other vocabularies.'),
'parents' => $this->t("The Drupal term IDs of the term's parents."),
'node_types' => $this->t('The names of the node types the vocabulary may be used with.'),
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Find node types for this row.
$node_types = $this->select('vocabulary_node_types', 'nt')
->fields('nt', ['type', 'vid'])
->condition('vid', $row->getSourceProperty('vid'))
->execute()
->fetchCol();
$row->setSourceProperty('node_types', $node_types);
$row->setSourceProperty('cardinality', ($row->getSourceProperty('tags') == 1 || $row->getSourceProperty('multiple') == 1) ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : 1);
// If the vocabulary being migrated is the one defined in the
// 'forum_nav_vocabulary' variable, set the 'forum_vocabulary' source
// property to true so we know this is the vocabulary used by Forum.
if ($this->variableGet('forum_nav_vocabulary', 0) == $row->getSourceProperty('vid')) {
$row->setSourceProperty('forum_vocabulary', TRUE);
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['vid']['type'] = 'integer';
return $ids;
}
}
@@ -0,0 +1,57 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
/**
* Gets all the vocabularies based on the node types that have Taxonomy enabled.
*
* @MigrateSource(
* id = "d6_taxonomy_vocabulary_per_type",
* source_module = "taxonomy"
* )
*/
class VocabularyPerType extends Vocabulary {
/**
* {@inheritdoc}
*/
public function query() {
$query = parent::query();
$query->join('vocabulary_node_types', 'nt', 'v.vid = nt.vid');
$query->fields('nt', ['type']);
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Get the i18n taxonomy translation setting for this vocabulary.
// 0 - No multilingual options
// 1 - Localizable terms. Run through the localization system.
// 2 - Predefined language for a vocabulary and its terms.
// 3 - Per-language terms, translatable (referencing terms with different
// languages) but not localizable.
$i18ntaxonomy_vocab = $this->variableGet('i18ntaxonomy_vocabulary', []);
$vid = $row->getSourceProperty('vid');
$i18ntaxonomy_vocabulary = FALSE;
if (array_key_exists($vid, $i18ntaxonomy_vocab)) {
$i18ntaxonomy_vocabulary = $i18ntaxonomy_vocab[$vid];
}
$row->setSourceProperty('i18ntaxonomy_vocabulary', $i18ntaxonomy_vocabulary);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['vid']['type'] = 'integer';
$ids['vid']['alias'] = 'nt';
$ids['type']['type'] = 'string';
return $ids;
}
}
@@ -0,0 +1,70 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 vocabulary translations from source database.
*
* @MigrateSource(
* id = "d6_taxonomy_vocabulary_translation",
* source_module = "i18ntaxonomy"
* )
*/
class VocabularyTranslation extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('vocabulary', 'v')
->fields('v')
->fields('i18n', ['lid', 'type', 'property', 'objectid'])
->fields('lt', ['lid', 'translation'])
->condition('i18n.type', 'vocabulary');
$query->addField('lt', 'language', 'lt.language');
// The i18n_strings table has two columns containing the object ID, objectid
// and objectindex. The objectid column is a text field. Therefore, for the
// join to work in PostgreSQL, use the objectindex field as this is numeric
// like the vid field.
$query->join('i18n_strings', 'i18n', 'v.vid = i18n.objectindex');
$query->innerJoin('locales_target', 'lt', 'lt.lid = i18n.lid');
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'vid' => $this->t('The vocabulary ID.'),
'language' => $this->t('Language for this field.'),
'property' => $this->t('Name of property being translated.'),
'translation' => $this->t('Translation of either the title or explanation.'),
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// For ease of reading the migration use 'language' as the property name for
// the language.
$language = $row->getSourceProperty('ltlanguage');
$row->setSourceProperty('language', $language);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['vid']['type'] = 'integer';
$ids['language']['type'] = 'string';
$ids['language']['alias'] = 'lt';
return $ids;
}
}
@@ -0,0 +1,138 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
/**
* Taxonomy term source from database.
*
* @todo Support term_relation, term_synonym table if possible.
*
* @MigrateSource(
* id = "d7_taxonomy_term",
* source_module = "taxonomy"
* )
*/
class Term extends FieldableEntity {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('taxonomy_term_data', 'td')
->fields('td')
->distinct()
->orderBy('tid');
$query->leftJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid');
$query->addField('tv', 'machine_name');
if ($this->getDatabase()
->schema()
->fieldExists('taxonomy_vocabulary', 'i18n_mode')) {
$query->addField('tv', 'i18n_mode');
}
if (isset($this->configuration['bundle'])) {
$query->condition('tv.machine_name', (array) $this->configuration['bundle'], 'IN');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'tid' => $this->t('The term ID.'),
'vid' => $this->t('Existing term VID'),
'machine_name' => $this->t('Vocabulary machine name'),
'name' => $this->t('The name of the term.'),
'description' => $this->t('The term description.'),
'weight' => $this->t('Weight'),
'parent' => $this->t("The Drupal term IDs of the term's parents."),
'format' => $this->t("Format of the term description."),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$tid = $row->getSourceProperty('tid');
$vocabulary = $row->getSourceProperty('machine_name');
$default_language = (array) $this->variableGet('language_default', ['language' => 'en']);
// If this entity was translated using Entity Translation, we need to get
// its source language to get the field values in the right language.
// The translations will be migrated by the d7_node_entity_translation
// migration.
$translatable_vocabularies = array_keys(array_filter($this->variableGet('entity_translation_taxonomy', [])));
$entity_translatable = $this->isEntityTranslatable('taxonomy_term') && in_array($vocabulary, $translatable_vocabularies, TRUE);
if ($entity_translatable) {
$source_language = $this->getEntityTranslationSourceLanguage('taxonomy_term', $tid);
$language = $entity_translatable && $source_language ? $source_language : $default_language['language'];
}
// If this is an i18n translation use the default language when i18n_mode
// is localized.
if ($row->get('i18n_mode')) {
$language = ($row->get('i18n_mode') === '1') ? $default_language['language'] : $row->get('language');
}
$language = $language ?? $default_language['language'];
$row->setSourceProperty('language', $language);
// Get Field API field values.
foreach ($this->getFields('taxonomy_term', $vocabulary) as $field_name => $field) {
// Ensure we're using the right language if the entity and the field are
// translatable.
$field_language = $entity_translatable && $field['translatable'] ? $language : NULL;
$row->setSourceProperty($field_name, $this->getFieldValues('taxonomy_term', $field_name, $tid, NULL, $field_language));
}
// Find parents for this row.
$parents = $this->select('taxonomy_term_hierarchy', 'th')
->fields('th', ['parent', 'tid'])
->condition('tid', $row->getSourceProperty('tid'))
->execute()
->fetchCol();
$row->setSourceProperty('parent', $parents);
// Determine if this is a forum container.
$forum_container_tids = $this->variableGet('forum_containers', []);
$current_tid = $row->getSourceProperty('tid');
$row->setSourceProperty('is_container', in_array($current_tid, $forum_container_tids));
// If the term name or term description were replaced by real fields using
// the Drupal 7 Title module, use the fields value instead of the term name
// or term description.
if ($this->moduleExists('title')) {
$name_field = $row->getSourceProperty('name_field');
if (isset($name_field[0]['value'])) {
$row->setSourceProperty('name', $name_field[0]['value']);
}
$description_field = $row->getSourceProperty('description_field');
if (isset($description_field[0]['value'])) {
$row->setSourceProperty('description', $description_field[0]['value']);
}
if (isset($description_field[0]['format'])) {
$row->setSourceProperty('format', $description_field[0]['format']);
}
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['tid']['type'] = 'integer';
return $ids;
}
}
@@ -0,0 +1,122 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
/**
* Provides Drupal 7 taxonomy term entity translation source plugin.
*
* @MigrateSource(
* id = "d7_taxonomy_term_entity_translation",
* source_module = "entity_translation"
* )
*/
class TermEntityTranslation extends FieldableEntity {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('entity_translation', 'et')
->fields('et')
->fields('td', [
'name',
'description',
'format',
])
->fields('tv', [
'machine_name',
])
->condition('et.entity_type', 'taxonomy_term')
->condition('et.source', '', '<>');
$query->innerJoin('taxonomy_term_data', 'td', 'td.tid = et.entity_id');
$query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid');
if (isset($this->configuration['bundle'])) {
$query->condition('tv.machine_name', (array) $this->configuration['bundle'], 'IN');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$tid = $row->getSourceProperty('entity_id');
$vocabulary = $row->getSourceProperty('machine_name');
$language = $row->getSourceProperty('language');
// Get Field API field values.
foreach ($this->getFields('taxonomy_term', $vocabulary) as $field_name => $field) {
// Ensure we're using the right language if the entity is translatable.
$field_language = $field['translatable'] ? $language : NULL;
$row->setSourceProperty($field_name, $this->getFieldValues('taxonomy_term', $field_name, $tid, NULL, $field_language));
}
// If the term name or term description were replaced by real fields using
// the Drupal 7 Title module, use the fields value instead of the term name
// or term description.
if ($this->moduleExists('title')) {
$name_field = $row->getSourceProperty('name_field');
if (isset($name_field[0]['value'])) {
$row->setSourceProperty('name', $name_field[0]['value']);
}
$description_field = $row->getSourceProperty('description_field');
if (isset($description_field[0]['value'])) {
$row->setSourceProperty('description', $description_field[0]['value']);
}
if (isset($description_field[0]['format'])) {
$row->setSourceProperty('format', $description_field[0]['format']);
}
}
// Determine if this is a forum container.
$forum_container_tids = $this->variableGet('forum_containers', []);
$row->setSourceProperty('is_container', in_array($tid, $forum_container_tids));
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'entity_type' => $this->t('The entity type this translation relates to'),
'entity_id' => $this->t('The entity ID this translation relates to'),
'revision_id' => $this->t('The entity revision ID this translation relates to'),
'language' => $this->t('The target language for this translation.'),
'source' => $this->t('The source language from which this translation was created.'),
'uid' => $this->t('The author of this translation.'),
'status' => $this->t('Boolean indicating whether the translation is published (visible to non-administrators).'),
'translate' => $this->t('A boolean indicating whether this translation needs to be updated.'),
'created' => $this->t('The Unix timestamp when the translation was created.'),
'changed' => $this->t('The Unix timestamp when the translation was most recently saved.'),
'name' => $this->t('The name of the term.'),
'description' => $this->t('The term description.'),
'format' => $this->t('Format of the term description.'),
'machine_name' => $this->t('Vocabulary machine name'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'entity_id' => [
'type' => 'integer',
'alias' => 'et',
],
'language' => [
'type' => 'string',
'alias' => 'et',
],
];
}
}
@@ -0,0 +1,108 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d7;
use Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait;
use Drupal\migrate\Row;
/**
* Gets i18n taxonomy terms from source database.
*
* @MigrateSource(
* id = "d7_term_localized_translation",
* source_module = "i18n_taxonomy"
* )
*/
class TermLocalizedTranslation extends Term {
use I18nQueryTrait;
/**
* {@inheritdoc}
*/
public function query() {
// Ideally, the query would return rows for each language for each taxonomy
// term with the translations for both the name and description or just the
// name translation or just the description translation. That query quickly
// became complex and would be difficult to maintain.
// Therefore, build a query based on i18nstrings table where each row has
// the translation for only one property, either name or description. The
// method prepareRow() is then used to obtain the translation for the other
// property.
$query = parent::query();
$query->addField('td', 'language', 'td.language');
// Add in the property, which is either name or description.
// Cast td.tid as char for PostgreSQL compatibility.
$query->leftJoin('i18n_string', 'i18n', 'CAST(td.tid AS CHAR(255)) = i18n.objectid');
$query->condition('i18n.type', 'term');
$query->addField('i18n', 'lid');
$query->addField('i18n', 'property');
// Add in the translation for the property.
$query->innerJoin('locales_target', 'lt', 'i18n.lid = lt.lid');
$query->addField('lt', 'language', 'lt.language');
$query->addField('lt', 'translation');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$language = $row->getSourceProperty('ltlanguage');
$tid = $row->getSourceProperty('tid');
// If this row has been migrated it is a duplicate then skip it.
if ($this->idMap->lookupDestinationIds(['tid' => $tid, 'language' => $language])) {
return FALSE;
}
// Save the translation for the property already in the row.
$property_in_row = $row->getSourceProperty('property');
$row->setSourceProperty($property_in_row . '_translated', $row->getSourceProperty('translation'));
// Get the translation for the property not already in the row and save it
// in the row.
$property_not_in_row = ($property_in_row == 'name') ? 'description' : 'name';
// Get the translation, if one exists, for the property not already in the
// row.
$query = $this->select('i18n_string', 'i18n')
->fields('i18n', ['lid'])
->condition('i18n.property', $property_not_in_row);
$query->leftJoin('locales_target', 'lt', 'i18n.lid = lt.lid');
$query->condition('lt.language', $language);
$query->addField('lt', 'translation');
$results = $query->execute()->fetchAssoc();
if (!$results) {
$row->setSourceProperty($property_not_in_row . '_translated', NULL);
}
else {
$row->setSourceProperty($property_not_in_row . '_translated', $results['translation']);
}
parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'language' => $this->t('Language for this term.'),
'name_translated' => $this->t('Term name translation.'),
'description_translated' => $this->t('Term description translation.'),
];
return parent::fields() + $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['language']['type'] = 'string';
$ids['language']['alias'] = 'lt';
return parent::getIds() + $ids;
}
}
@@ -0,0 +1,63 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d7;
use Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait;
use Drupal\migrate\Row;
/**
* Gets i18n taxonomy terms from source database.
*
* @MigrateSource(
* id = "d7_taxonomy_term_translation",
* source_module = "i18n_taxonomy"
* )
*/
class TermTranslation extends Term {
use I18nQueryTrait;
/**
* {@inheritdoc}
*/
public function query() {
$query = parent::query();
if ($this->database->schema()->fieldExists('taxonomy_term_data', 'language')) {
$query->addField('td', 'language', 'td_language');
}
if ($this->database->schema()->fieldExists('taxonomy_vocabulary', 'i18n_mode')) {
$query->addField('tv', 'i18n_mode');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
parent::prepareRow($row);
$row->setSourceProperty('language', $row->getSourceProperty('td_language'));
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'language' => $this->t('Language for this term.'),
'name_translated' => $this->t('Term name translation.'),
'description_translated' => $this->t('Term description translation.'),
];
return parent::fields() + $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['language']['type'] = 'string';
$ids['language']['alias'] = 'td';
return parent::getIds() + $ids;
}
}
@@ -0,0 +1,72 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 7 vocabularies source from database.
*
* @MigrateSource(
* id = "d7_taxonomy_vocabulary",
* source_module = "taxonomy"
* )
*/
class Vocabulary extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('taxonomy_vocabulary', 'v')
->fields('v', [
'vid',
'name',
'description',
'hierarchy',
'module',
'weight',
'machine_name',
]);
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'vid' => $this->t('The vocabulary ID.'),
'name' => $this->t('The name of the vocabulary.'),
'description' => $this->t('The description of the vocabulary.'),
'hierarchy' => $this->t('The type of hierarchy allowed within the vocabulary. (0 = disabled, 1 = single, 2 = multiple)'),
'module' => $this->t('Module responsible for the vocabulary.'),
'weight' => $this->t('The weight of the vocabulary in relation to other vocabularies.'),
'machine_name' => $this->t('Unique machine name of the vocabulary.'),
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// If the vocabulary being migrated is the one defined in the
// 'forum_nav_vocabulary' variable, set the 'forum_vocabulary' source
// property to true so we know this is the vocabulary used by Forum.
if ($this->variableGet('forum_nav_vocabulary', 0) == $row->getSourceProperty('vid')) {
$row->setSourceProperty('forum_vocabulary', TRUE);
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['vid']['type'] = 'integer';
return $ids;
}
}
@@ -0,0 +1,77 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d7;
/**
* Drupal 7 vocabulary translations from source database.
*
* @MigrateSource(
* id = "d7_taxonomy_vocabulary_translation",
* source_module = "i18n_taxonomy"
* )
*/
class VocabularyTranslation extends Vocabulary {
/**
* {@inheritdoc}
*/
public function query() {
$query = parent::query();
$query->leftjoin('i18n_string', 'i18n', 'CAST (v.vid AS CHAR(222))= i18n.objectid');
$query->innerJoin('locales_target', 'lt', 'lt.lid = i18n.lid');
$query
->condition('type', 'vocabulary')
->fields('lt')
->fields('i18n');
$query->addField('lt', 'lid', 'lt_lid');
if ($this->getDatabase()
->schema()
->fieldExists('taxonomy_vocabulary', 'language')) {
$query->addField('v', 'language', 'v_language');
}
if ($this->getDatabase()
->schema()
->fieldExists('taxonomy_vocabulary', 'i18n_mode')) {
$query->addField('v', 'i18n_mode');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'i18n_mode' => $this->t('Internationalization mode.'),
'v_language' => $this->t('Language from the taxonomy_vocabulary table.'),
'property' => $this->t('Name of property being translated.'),
'type' => $this->t('Name of property being translated.'),
'objectid' => $this->t('Name of property being translated.'),
'lt_lid' => $this->t('Name of property being translated.'),
'translation' => $this->t('Translation of either the name or the description.'),
'lid' => $this->t('Language string ID'),
'textgroup' => $this->t('A module defined group of translations'),
'context' => $this->t('Full string ID for quick search: type:objectid:property.'),
'objectindex' => $this->t('Integer value of Object ID'),
'format' => $this->t('The {filter_format}.format of the string'),
'language' => $this->t('Language code from locales_target table'),
'plid' => $this->t('Parent lid'),
'plural' => $this->t('Plural index number'),
'i18n_status' => $this->t('Translation needs update'),
];
return parent::fields() + $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids = [];
$ids['language']['type'] = 'string';
$ids['language']['alias'] = 'lt';
$ids['property']['type'] = 'string';
return parent::getIds() + $ids;
}
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument;
use Drupal\taxonomy\Entity\Term;
use Drupal\views\Plugin\views\argument\ManyToOne;
/**
* Allow taxonomy term ID(s) as argument.
*
* @ingroup views_argument_handlers
*
* @ViewsArgument("taxonomy_index_tid")
*/
class IndexTid extends ManyToOne {
public function titleQuery() {
$titles = [];
$terms = Term::loadMultiple($this->value);
foreach ($terms as $term) {
$titles[] = \Drupal::service('entity.repository')->getTranslationFromContext($term)->label();
}
return $titles;
}
}
@@ -0,0 +1,150 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\views\Plugin\views\argument\ArgumentPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Argument handler for taxonomy terms with depth.
*
* This handler is actually part of the node table and has some restrictions,
* because it uses a subquery to find nodes with.
*
* @ingroup views_argument_handlers
*
* @ViewsArgument("taxonomy_index_tid_depth")
*/
class IndexTidDepth extends ArgumentPluginBase implements ContainerFactoryPluginInterface {
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $termStorage;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $termStorage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->termStorage = $termStorage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('taxonomy_term')
);
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['depth'] = ['default' => 0];
$options['break_phrase'] = ['default' => FALSE];
$options['use_taxonomy_term_path'] = ['default' => FALSE];
return $options;
}
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['depth'] = [
'#type' => 'weight',
'#title' => $this->t('Depth'),
'#default_value' => $this->options['depth'],
'#description' => $this->t('The depth will match nodes tagged with terms in the hierarchy. For example, if you have the term "fruit" and a child term "apple", with a depth of 1 (or higher) then filtering for the term "fruit" will get nodes that are tagged with "apple" as well as "fruit". If negative, the reverse is true; searching for "apple" will also pick up nodes tagged with "fruit" if depth is -1 (or lower).'),
];
$form['break_phrase'] = [
'#type' => 'checkbox',
'#title' => $this->t('Allow multiple values'),
'#description' => $this->t('If selected, users can enter multiple values in the form of 1+2+3. Due to the number of JOINs it would require, AND will be treated as OR with this filter.'),
'#default_value' => !empty($this->options['break_phrase']),
];
parent::buildOptionsForm($form, $form_state);
}
/**
* Override defaultActions() to remove summary actions.
*/
protected function defaultActions($which = NULL) {
if ($which) {
if (in_array($which, ['ignore', 'not found', 'empty', 'default'])) {
return parent::defaultActions($which);
}
return;
}
$actions = parent::defaultActions();
unset($actions['summary asc']);
unset($actions['summary desc']);
unset($actions['summary asc by count']);
unset($actions['summary desc by count']);
return $actions;
}
public function query($group_by = FALSE) {
$this->ensureMyTable();
if (!empty($this->options['break_phrase'])) {
$break = static::breakString($this->argument);
if ($break->value === [-1]) {
return FALSE;
}
$operator = (count($break->value) > 1) ? 'IN' : '=';
$tids = $break->value;
}
else {
$operator = "=";
$tids = $this->argument;
}
// Now build the subqueries.
$subquery = Database::getConnection()->select('taxonomy_index', 'tn');
$subquery->addField('tn', 'nid');
$where = (new Condition('OR'))->condition('tn.tid', $tids, $operator);
$last = "tn";
if ($this->options['depth'] > 0) {
$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__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) {
$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";
}
}
$subquery->condition($where);
$this->query->addWhere(0, "$this->tableAlias.$this->realField", $subquery, 'IN');
}
public function title() {
$term = $this->termStorage->load($this->argument);
if (!empty($term)) {
return $term->getName();
}
// TODO review text
return $this->t('No name');
}
}
@@ -0,0 +1,70 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\argument\ArgumentPluginBase;
/**
* Argument handler for to modify depth for a previous term.
*
* This handler is actually part of the node table and has some restrictions,
* because it uses a subquery to find nodes with.
*
* @ingroup views_argument_handlers
*
* @ViewsArgument("taxonomy_index_tid_depth_modifier")
*/
class IndexTidDepthModifier extends ArgumentPluginBase {
public function buildOptionsForm(&$form, FormStateInterface $form_state) {}
public function query($group_by = FALSE) {}
public function preQuery() {
// We don't know our argument yet, but it's based upon our position:
$argument = isset($this->view->args[$this->position]) ? $this->view->args[$this->position] : NULL;
if (!is_numeric($argument)) {
return;
}
if ($argument > 10) {
$argument = 10;
}
if ($argument < -10) {
$argument = -10;
}
// figure out which argument preceded us.
$keys = array_reverse(array_keys($this->view->argument));
$skip = TRUE;
foreach ($keys as $key) {
if ($key == $this->options['id']) {
$skip = FALSE;
continue;
}
if ($skip) {
continue;
}
if (empty($this->view->argument[$key])) {
continue;
}
if (isset($handler)) {
unset($handler);
}
$handler = &$this->view->argument[$key];
if (empty($handler->definition['accept depth modifier'])) {
continue;
}
// Finally!
$handler->options['depth'] = $argument;
}
}
}
@@ -0,0 +1,60 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\views\Plugin\views\argument\NumericArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Argument handler for basic taxonomy tid.
*
* @ingroup views_argument_handlers
*
* @ViewsArgument("taxonomy")
*/
class Taxonomy extends NumericArgument implements ContainerFactoryPluginInterface {
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $termStorage;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $term_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->termStorage = $term_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('taxonomy_term')
);
}
/**
* Override the behavior of title(). Get the title of the node.
*/
public function title() {
// There might be no valid argument.
if ($this->argument) {
$term = $this->termStorage->load($this->argument);
if (!empty($term)) {
return $term->getName();
}
}
// TODO review text
return $this->t('No name');
}
}
@@ -0,0 +1,66 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument;
use Drupal\views\Plugin\views\argument\NumericArgument;
use Drupal\taxonomy\VocabularyStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Argument handler to accept a vocabulary id.
*
* @ingroup views_argument_handlers
*
* @ViewsArgument("vocabulary_vid")
*/
class VocabularyVid extends NumericArgument {
/**
* The vocabulary storage.
*
* @var \Drupal\taxonomy\VocabularyStorageInterface
*/
protected $vocabularyStorage;
/**
* Constructs the VocabularyVid object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage
* The vocabulary storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, VocabularyStorageInterface $vocabulary_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->vocabularyStorage = $vocabulary_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('taxonomy_vocabulary')
);
}
/**
* Override the behavior of title(). Get the name of the vocabulary.
*/
public function title() {
$vocabulary = $this->vocabularyStorage->load($this->argument);
if ($vocabulary) {
return $vocabulary->label();
}
return $this->t('No vocabulary');
}
}
@@ -0,0 +1,245 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument_default;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\taxonomy\TermInterface;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\taxonomy\VocabularyStorageInterface;
/**
* Taxonomy tid default argument.
*
* @ViewsArgumentDefault(
* id = "taxonomy_tid",
* title = @Translation("Taxonomy term ID from URL")
* )
*/
class Tid extends ArgumentDefaultPluginBase implements CacheableDependencyInterface {
/**
* The route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The vocabulary storage.
*
* @var \Drupal\taxonomy\VocabularyStorageInterface
*/
protected $vocabularyStorage;
/**
* Constructs a new Tid instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition. *
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage
* The vocabulary storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match, VocabularyStorageInterface $vocabulary_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->routeMatch = $route_match;
$this->vocabularyStorage = $vocabulary_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('current_route_match'),
$container->get('entity_type.manager')->getStorage('taxonomy_vocabulary')
);
}
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
// @todo Remove the legacy code.
// Convert legacy vids option to machine name vocabularies.
if (!empty($this->options['vids'])) {
$vocabularies = taxonomy_vocabulary_get_names();
foreach ($this->options['vids'] as $vid) {
if (isset($vocabularies[$vid], $vocabularies[$vid]->machine_name)) {
$this->options['vocabularies'][$vocabularies[$vid]->machine_name] = $vocabularies[$vid]->machine_name;
}
}
}
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['term_page'] = ['default' => TRUE];
$options['node'] = ['default' => FALSE];
$options['anyall'] = ['default' => ','];
$options['limit'] = ['default' => FALSE];
$options['vids'] = ['default' => []];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['term_page'] = [
'#type' => 'checkbox',
'#title' => $this->t('Load default filter from term page'),
'#default_value' => $this->options['term_page'],
];
$form['node'] = [
'#type' => 'checkbox',
'#title' => $this->t("Load default filter from node page, that's good for related taxonomy blocks"),
'#default_value' => $this->options['node'],
];
$form['limit'] = [
'#type' => 'checkbox',
'#title' => $this->t('Limit terms by vocabulary'),
'#default_value' => $this->options['limit'],
'#states' => [
'visible' => [
':input[name="options[argument_default][taxonomy_tid][node]"]' => ['checked' => TRUE],
],
],
];
$options = [];
$vocabularies = $this->vocabularyStorage->loadMultiple();
foreach ($vocabularies as $voc) {
$options[$voc->id()] = $voc->label();
}
$form['vids'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Vocabularies'),
'#options' => $options,
'#default_value' => $this->options['vids'],
'#states' => [
'visible' => [
':input[name="options[argument_default][taxonomy_tid][limit]"]' => ['checked' => TRUE],
':input[name="options[argument_default][taxonomy_tid][node]"]' => ['checked' => TRUE],
],
],
];
$form['anyall'] = [
'#type' => 'radios',
'#title' => $this->t('Multiple-value handling'),
'#default_value' => $this->options['anyall'],
'#options' => [
',' => $this->t('Filter to items that share all terms'),
'+' => $this->t('Filter to items that share any term'),
],
'#states' => [
'visible' => [
':input[name="options[argument_default][taxonomy_tid][node]"]' => ['checked' => TRUE],
],
],
];
}
/**
* {@inheritdoc}
*/
public function submitOptionsForm(&$form, FormStateInterface $form_state, &$options = []) {
// Filter unselected items so we don't unnecessarily store giant arrays.
$options['vids'] = array_filter($options['vids']);
}
/**
* {@inheritdoc}
*/
public function getArgument() {
// Load default argument from taxonomy page.
if (!empty($this->options['term_page'])) {
if (($taxonomy_term = $this->routeMatch->getParameter('taxonomy_term')) && $taxonomy_term instanceof TermInterface) {
return $taxonomy_term->id();
}
}
// Load default argument from node.
if (!empty($this->options['node'])) {
// Just check, if a node could be detected.
if (($node = $this->routeMatch->getParameter('node')) && $node instanceof NodeInterface) {
$taxonomy = [];
foreach ($node->getFieldDefinitions() as $field) {
if ($field->getType() == 'entity_reference' && $field->getSetting('target_type') == 'taxonomy_term') {
$taxonomy_terms = $node->{$field->getName()}->referencedEntities();
/** @var \Drupal\taxonomy\TermInterface $taxonomy_term */
foreach ($taxonomy_terms as $taxonomy_term) {
$taxonomy[$taxonomy_term->id()] = $taxonomy_term->bundle();
}
}
}
if (!empty($this->options['limit'])) {
$tids = [];
// filter by vocabulary
foreach ($taxonomy as $tid => $vocab) {
if (!empty($this->options['vids'][$vocab])) {
$tids[] = $tid;
}
}
return implode($this->options['anyall'], $tids);
}
// Return all tids.
else {
return implode($this->options['anyall'], array_keys($taxonomy));
}
}
}
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['url'];
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
foreach ($this->vocabularyStorage->loadMultiple(array_keys($this->options['vids'])) as $vocabulary) {
$dependencies[$vocabulary->getConfigDependencyKey()][] = $vocabulary->getConfigDependencyName();
}
return $dependencies;
}
}
@@ -0,0 +1,32 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument_validator;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\argument_validator\Entity;
/**
* Adds legacy vocabulary handling to standard Entity Argument validation..
*/
class Term extends Entity {
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
// @todo Remove the legacy code.
// Convert legacy vids option to machine name vocabularies.
if (!empty($this->options['vids'])) {
$vocabularies = taxonomy_vocabulary_get_names();
foreach ($this->options['vids'] as $vid) {
if (isset($vocabularies[$vid], $vocabularies[$vid]->machine_name)) {
$this->options['vocabularies'][$vocabularies[$vid]->machine_name] = $vocabularies[$vid]->machine_name;
}
}
}
}
}
@@ -0,0 +1,87 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument_validator;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\argument_validator\Entity;
/**
* Validates whether a term name is a valid term argument.
*
* @ViewsArgumentValidator(
* id = "taxonomy_term_name",
* title = @Translation("Taxonomy term name"),
* entity_type = "taxonomy_term"
* )
*/
class TermName extends Entity {
/**
* The taxonomy term storage.
*
* @var \Drupal\taxonomy\TermStorageInterface
*/
protected $termStorage;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_type_bundle_info);
// Not handling exploding term names.
$this->multipleCapable = FALSE;
$this->termStorage = $entity_type_manager->getStorage('taxonomy_term');
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['transform'] = ['default' => FALSE];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$form['transform'] = [
'#type' => 'checkbox',
'#title' => $this->t('Transform dashes in URL to spaces in term name filter values'),
'#default_value' => $this->options['transform'],
];
}
/**
* {@inheritdoc}
*/
public function validateArgument($argument) {
if ($this->options['transform']) {
$argument = str_replace('-', ' ', $argument);
$this->argument->argument = $argument;
}
$terms = $this->termStorage->loadByProperties(['name' => $argument]);
if (!$terms) {
// Returned empty array no terms with the name.
return FALSE;
}
// Not knowing which term will be used if more than one is returned check
// each one.
foreach ($terms as $term) {
if (!$this->validateEntity($term)) {
return FALSE;
}
}
return TRUE;
}
}
@@ -0,0 +1,177 @@
<?php
namespace Drupal\taxonomy\Plugin\views\field;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\field\PrerenderList;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\taxonomy\VocabularyStorageInterface;
/**
* Field handler to display all taxonomy terms of a node.
*
* @ingroup views_field_handlers
*
* @ViewsField("taxonomy_index_tid")
*/
class TaxonomyIndexTid extends PrerenderList {
/**
* The vocabulary storage.
*
* @var \Drupal\taxonomy\VocabularyStorageInterface
*/
protected $vocabularyStorage;
/**
* Constructs a TaxonomyIndexTid object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage
* The vocabulary storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, VocabularyStorageInterface $vocabulary_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->vocabularyStorage = $vocabulary_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('taxonomy_vocabulary')
);
}
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
// @todo: Wouldn't it be possible to use $this->base_table and no if here?
if ($view->storage->get('base_table') == 'node_field_revision') {
$this->additional_fields['nid'] = ['table' => 'node_field_revision', 'field' => 'nid'];
}
else {
$this->additional_fields['nid'] = ['table' => 'node_field_data', 'field' => 'nid'];
}
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['link_to_taxonomy'] = ['default' => TRUE];
$options['limit'] = ['default' => FALSE];
$options['vids'] = ['default' => []];
return $options;
}
/**
* Provide "link to term" option.
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['link_to_taxonomy'] = [
'#title' => $this->t('Link this field to its term page'),
'#type' => 'checkbox',
'#default_value' => !empty($this->options['link_to_taxonomy']),
];
$form['limit'] = [
'#type' => 'checkbox',
'#title' => $this->t('Limit terms by vocabulary'),
'#default_value' => $this->options['limit'],
];
$options = [];
$vocabularies = $this->vocabularyStorage->loadMultiple();
foreach ($vocabularies as $voc) {
$options[$voc->id()] = $voc->label();
}
$form['vids'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Vocabularies'),
'#options' => $options,
'#default_value' => $this->options['vids'],
'#states' => [
'visible' => [
':input[name="options[limit]"]' => ['checked' => TRUE],
],
],
];
parent::buildOptionsForm($form, $form_state);
}
/**
* Add this term to the query
*/
public function query() {
$this->addAdditionalFields();
}
public function preRender(&$values) {
$vocabularies = $this->vocabularyStorage->loadMultiple();
$this->field_alias = $this->aliases['nid'];
$nids = [];
foreach ($values as $result) {
if (!empty($result->{$this->aliases['nid']})) {
$nids[] = $result->{$this->aliases['nid']};
}
}
if ($nids) {
$vocabs = array_filter($this->options['vids']);
if (empty($this->options['limit'])) {
$vocabs = [];
}
$result = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->getNodeTerms($nids, $vocabs);
foreach ($result as $node_nid => $data) {
foreach ($data as $tid => $term) {
$this->items[$node_nid][$tid]['name'] = \Drupal::service('entity.repository')->getTranslationFromContext($term)->label();
$this->items[$node_nid][$tid]['tid'] = $tid;
$this->items[$node_nid][$tid]['vocabulary_vid'] = $term->bundle();
$this->items[$node_nid][$tid]['vocabulary'] = $vocabularies[$term->bundle()]->label();
if (!empty($this->options['link_to_taxonomy'])) {
$this->items[$node_nid][$tid]['make_link'] = TRUE;
$this->items[$node_nid][$tid]['path'] = 'taxonomy/term/' . $tid;
}
}
}
}
}
public function render_item($count, $item) {
return $item['name'];
}
protected function documentSelfTokens(&$tokens) {
$tokens['{{ ' . $this->options['id'] . '__tid' . ' }}'] = $this->t('The taxonomy term ID for the term.');
$tokens['{{ ' . $this->options['id'] . '__name' . ' }}'] = $this->t('The taxonomy term name for the term.');
$tokens['{{ ' . $this->options['id'] . '__vocabulary_vid' . ' }}'] = $this->t('The machine name for the vocabulary the term belongs to.');
$tokens['{{ ' . $this->options['id'] . '__vocabulary' . ' }}'] = $this->t('The name for the vocabulary the term belongs to.');
}
protected function addSelfTokens(&$tokens, $item) {
foreach (['tid', 'name', 'vocabulary_vid', 'vocabulary'] as $token) {
$tokens['{{ ' . $this->options['id'] . '__' . $token . ' }}'] = isset($item[$token]) ? $item[$token] : '';
}
}
}
@@ -0,0 +1,56 @@
<?php
namespace Drupal\taxonomy\Plugin\views\field;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\field\EntityField;
use Drupal\views\ResultRow;
/**
* Displays taxonomy term names and allows converting spaces to hyphens.
*
* @ingroup views_field_handlers
*
* @ViewsField("term_name")
*/
class TermName extends EntityField {
/**
* {@inheritdoc}
*/
public function getItems(ResultRow $values) {
$items = parent::getItems($values);
if ($this->options['convert_spaces']) {
foreach ($items as &$item) {
// Replace spaces with hyphens.
$name = $item['raw']->get('value')->getValue();
// @todo Add link support https://www.drupal.org/node/2567745
$item['rendered']['#context']['value'] = str_replace(' ', '-', $name);
}
}
return $items;
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['convert_spaces'] = ['default' => FALSE];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['convert_spaces'] = [
'#title' => $this->t('Convert spaces in term names to hyphens'),
'#type' => 'checkbox',
'#default_value' => !empty($this->options['convert_spaces']),
];
parent::buildOptionsForm($form, $form_state);
}
}
@@ -0,0 +1,403 @@
<?php
namespace Drupal\taxonomy\Plugin\views\filter;
use Drupal\Core\Entity\Element\EntityAutocomplete;
use Drupal\Core\Form\FormStateInterface;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\TermStorageInterface;
use Drupal\taxonomy\VocabularyStorageInterface;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\filter\ManyToOne;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Filter by term id.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("taxonomy_index_tid")
*/
class TaxonomyIndexTid extends ManyToOne {
// Stores the exposed input for this filter.
public $validated_exposed_input = NULL;
/**
* The vocabulary storage.
*
* @var \Drupal\taxonomy\VocabularyStorageInterface
*/
protected $vocabularyStorage;
/**
* The term storage.
*
* @var \Drupal\taxonomy\TermStorageInterface
*/
protected $termStorage;
/**
* Constructs a TaxonomyIndexTid object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage
* The vocabulary storage.
* @param \Drupal\taxonomy\TermStorageInterface $term_storage
* The term storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, VocabularyStorageInterface $vocabulary_storage, TermStorageInterface $term_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->vocabularyStorage = $vocabulary_storage;
$this->termStorage = $term_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('taxonomy_vocabulary'),
$container->get('entity_type.manager')->getStorage('taxonomy_term')
);
}
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
if (!empty($this->definition['vocabulary'])) {
$this->options['vid'] = $this->definition['vocabulary'];
}
}
public function hasExtraOptions() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getValueOptions() {
return $this->valueOptions;
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['type'] = ['default' => 'textfield'];
$options['limit'] = ['default' => TRUE];
$options['vid'] = ['default' => ''];
$options['hierarchy'] = ['default' => FALSE];
$options['error_message'] = ['default' => TRUE];
return $options;
}
public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) {
$vocabularies = $this->vocabularyStorage->loadMultiple();
$options = [];
foreach ($vocabularies as $voc) {
$options[$voc->id()] = $voc->label();
}
if ($this->options['limit']) {
// We only do this when the form is displayed.
if (empty($this->options['vid'])) {
$first_vocabulary = reset($vocabularies);
$this->options['vid'] = $first_vocabulary->id();
}
if (empty($this->definition['vocabulary'])) {
$form['vid'] = [
'#type' => 'radios',
'#title' => $this->t('Vocabulary'),
'#options' => $options,
'#description' => $this->t('Select which vocabulary to show terms for in the regular options.'),
'#default_value' => $this->options['vid'],
];
}
}
$form['type'] = [
'#type' => 'radios',
'#title' => $this->t('Selection type'),
'#options' => ['select' => $this->t('Dropdown'), 'textfield' => $this->t('Autocomplete')],
'#default_value' => $this->options['type'],
];
$form['hierarchy'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show hierarchy in dropdown'),
'#default_value' => !empty($this->options['hierarchy']),
'#states' => [
'visible' => [
':input[name="options[type]"]' => ['value' => 'select'],
],
],
];
}
protected function valueForm(&$form, FormStateInterface $form_state) {
$vocabulary = $this->vocabularyStorage->load($this->options['vid']);
if (empty($vocabulary) && $this->options['limit']) {
$form['markup'] = [
'#markup' => '<div class="js-form-item form-item">' . $this->t('An invalid vocabulary is selected. Please change it in the options.') . '</div>',
];
return;
}
if ($this->options['type'] == 'textfield') {
$terms = $this->value ? Term::loadMultiple(($this->value)) : [];
$form['value'] = [
'#title' => $this->options['limit'] ? $this->t('Select terms from vocabulary @voc', ['@voc' => $vocabulary->label()]) : $this->t('Select terms'),
'#type' => 'textfield',
'#default_value' => EntityAutocomplete::getEntityLabels($terms),
];
if ($this->options['limit']) {
$form['value']['#type'] = 'entity_autocomplete';
$form['value']['#target_type'] = 'taxonomy_term';
$form['value']['#selection_settings']['target_bundles'] = [$vocabulary->id()];
$form['value']['#tags'] = TRUE;
$form['value']['#process_default_value'] = FALSE;
}
}
else {
if (!empty($this->options['hierarchy']) && $this->options['limit']) {
$tree = $this->termStorage->loadTree($vocabulary->id(), 0, NULL, TRUE);
$options = [];
if ($tree) {
foreach ($tree as $term) {
$choice = new \stdClass();
$choice->option = [$term->id() => str_repeat('-', $term->depth) . \Drupal::service('entity.repository')->getTranslationFromContext($term)->label()];
$options[] = $choice;
}
}
}
else {
$options = [];
$query = \Drupal::entityQuery('taxonomy_term')
// @todo Sorting on vocabulary properties -
// https://www.drupal.org/node/1821274.
->sort('weight')
->sort('name')
->addTag('taxonomy_term_access');
if ($this->options['limit']) {
$query->condition('vid', $vocabulary->id());
}
$terms = Term::loadMultiple($query->execute());
foreach ($terms as $term) {
$options[$term->id()] = \Drupal::service('entity.repository')->getTranslationFromContext($term)->label();
}
}
$default_value = (array) $this->value;
if ($exposed = $form_state->get('exposed')) {
$identifier = $this->options['expose']['identifier'];
if (!empty($this->options['expose']['reduce'])) {
$options = $this->reduceValueOptions($options);
if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
$default_value = [];
}
}
if (empty($this->options['expose']['multiple'])) {
if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) {
$default_value = 'All';
}
elseif (empty($default_value)) {
$keys = array_keys($options);
$default_value = array_shift($keys);
}
// Due to #1464174 there is a chance that array('') was saved in the admin ui.
// Let's choose a safe default value.
elseif ($default_value == ['']) {
$default_value = 'All';
}
else {
$copy = $default_value;
$default_value = array_shift($copy);
}
}
}
$form['value'] = [
'#type' => 'select',
'#title' => $this->options['limit'] ? $this->t('Select terms from vocabulary @voc', ['@voc' => $vocabulary->label()]) : $this->t('Select terms'),
'#multiple' => TRUE,
'#options' => $options,
'#size' => min(9, count($options)),
'#default_value' => $default_value,
];
$user_input = $form_state->getUserInput();
if ($exposed && isset($identifier) && !isset($user_input[$identifier])) {
$user_input[$identifier] = $default_value;
$form_state->setUserInput($user_input);
}
}
if (!$form_state->get('exposed')) {
// Retain the helper option
$this->helper->buildOptionsForm($form, $form_state);
// Show help text if not exposed to end users.
$form['value']['#description'] = t('Leave blank for all. Otherwise, the first selected term will be the default instead of "Any".');
}
}
protected function valueValidate($form, FormStateInterface $form_state) {
// We only validate if they've chosen the text field style.
if ($this->options['type'] != 'textfield') {
return;
}
$tids = [];
if ($values = $form_state->getValue(['options', 'value'])) {
foreach ($values as $value) {
$tids[] = $value['target_id'];
}
}
$form_state->setValue(['options', 'value'], $tids);
}
public function acceptExposedInput($input) {
if (empty($this->options['exposed'])) {
return TRUE;
}
// We need to know the operator, which is normally set in
// \Drupal\views\Plugin\views\filter\FilterPluginBase::acceptExposedInput(),
// before we actually call the parent version of ourselves.
if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) {
$this->operator = $input[$this->options['expose']['operator_id']];
}
// If view is an attachment and is inheriting exposed filters, then assume
// exposed input has already been validated
if (!empty($this->view->is_attachment) && $this->view->display_handler->usesExposed()) {
$this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']];
}
// If we're checking for EMPTY or NOT, we don't need any input, and we can
// say that our input conditions are met by just having the right operator.
if ($this->operator == 'empty' || $this->operator == 'not empty') {
return TRUE;
}
// If it's non-required and there's no value don't bother filtering.
if (!$this->options['expose']['required'] && empty($this->validated_exposed_input)) {
return FALSE;
}
$rc = parent::acceptExposedInput($input);
if ($rc) {
// If we have previously validated input, override.
if (isset($this->validated_exposed_input)) {
$this->value = $this->validated_exposed_input;
}
}
return $rc;
}
public function validateExposed(&$form, FormStateInterface $form_state) {
if (empty($this->options['exposed'])) {
return;
}
$identifier = $this->options['expose']['identifier'];
// We only validate if they've chosen the text field style.
if ($this->options['type'] != 'textfield') {
if ($form_state->getValue($identifier) != 'All') {
$this->validated_exposed_input = (array) $form_state->getValue($identifier);
}
return;
}
if (empty($this->options['expose']['identifier'])) {
return;
}
if ($values = $form_state->getValue($identifier)) {
foreach ($values as $value) {
$this->validated_exposed_input[] = $value['target_id'];
}
}
}
protected function valueSubmit($form, FormStateInterface $form_state) {
// prevent array_filter from messing up our arrays in parent submit.
}
public function buildExposeForm(&$form, FormStateInterface $form_state) {
parent::buildExposeForm($form, $form_state);
if ($this->options['type'] != 'select') {
unset($form['expose']['reduce']);
}
$form['error_message'] = [
'#type' => 'checkbox',
'#title' => $this->t('Display error message'),
'#default_value' => !empty($this->options['error_message']),
];
}
public function adminSummary() {
// set up $this->valueOptions for the parent summary
$this->valueOptions = [];
if ($this->value) {
$this->value = array_filter($this->value);
$terms = Term::loadMultiple($this->value);
foreach ($terms as $term) {
$this->valueOptions[$term->id()] = \Drupal::service('entity.repository')->getTranslationFromContext($term)->label();
}
}
return parent::adminSummary();
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
$contexts = parent::getCacheContexts();
// The result potentially depends on term access and so is just cacheable
// per user.
// @todo See https://www.drupal.org/node/2352175.
$contexts[] = 'user';
return $contexts;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
$vocabulary = $this->vocabularyStorage->load($this->options['vid']);
$dependencies[$vocabulary->getConfigDependencyKey()][] = $vocabulary->getConfigDependencyName();
foreach ($this->termStorage->loadMultiple($this->options['value']) as $term) {
$dependencies[$term->getConfigDependencyKey()][] = $term->getConfigDependencyName();
}
return $dependencies;
}
}
@@ -0,0 +1,102 @@
<?php
namespace Drupal\taxonomy\Plugin\views\filter;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Form\FormStateInterface;
/**
* Filter handler for taxonomy terms with depth.
*
* This handler is actually part of the node table and has some restrictions,
* because it uses a subquery to find nodes with.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("taxonomy_index_tid_depth")
*/
class TaxonomyIndexTidDepth extends TaxonomyIndexTid {
public function operatorOptions($which = 'title') {
return [
'or' => $this->t('Is one of'),
];
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['depth'] = ['default' => 0];
return $options;
}
public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildExtraOptionsForm($form, $form_state);
$form['depth'] = [
'#type' => 'weight',
'#title' => $this->t('Depth'),
'#default_value' => $this->options['depth'],
'#description' => $this->t('The depth will match nodes tagged with terms in the hierarchy. For example, if you have the term "fruit" and a child term "apple", with a depth of 1 (or higher) then filtering for the term "fruit" will get nodes that are tagged with "apple" as well as "fruit". If negative, the reverse is true; searching for "apple" will also pick up nodes tagged with "fruit" if depth is -1 (or lower).'),
];
}
public function query() {
// If no filter values are present, then do nothing.
if (count($this->value) == 0) {
return;
}
elseif (count($this->value) == 1) {
// Sometimes $this->value is an array with a single element so convert it.
if (is_array($this->value)) {
$this->value = current($this->value);
}
$operator = '=';
}
else {
$operator = 'IN';
}
// The normal use of ensureMyTable() here breaks Views.
// So instead we trick the filter into using the alias of the base table.
// See https://www.drupal.org/node/271833.
// If a relationship is set, we must use the alias it provides.
if (!empty($this->relationship)) {
$this->tableAlias = $this->relationship;
}
// If no relationship, then use the alias of the base table.
else {
$this->tableAlias = $this->query->ensureTable($this->view->storage->get('base_table'));
}
// Now build the subqueries.
$subquery = Database::getConnection()->select('taxonomy_index', 'tn');
$subquery->addField('tn', 'nid');
$where = (new Condition('OR'))->condition('tn.tid', $this->value, $operator);
$last = "tn";
if ($this->options['depth'] > 0) {
$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__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) {
$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";
}
}
$subquery->condition($where);
$this->query->addWhere($this->options['group'], "$this->tableAlias.$this->realField", $subquery, 'IN');
}
}
@@ -0,0 +1,165 @@
<?php
namespace Drupal\taxonomy\Plugin\views\relationship;
use Drupal\Core\Database\Database;
use Drupal\Core\Form\FormStateInterface;
use Drupal\taxonomy\VocabularyStorageInterface;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\relationship\RelationshipPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Relationship handler to return the taxonomy terms of nodes.
*
* @ingroup views_relationship_handlers
*
* @ViewsRelationship("node_term_data")
*/
class NodeTermData extends RelationshipPluginBase {
/**
* The vocabulary storage.
*
* @var \Drupal\taxonomy\VocabularyStorageInterface
*/
protected $vocabularyStorage;
/**
* Constructs a NodeTermData object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage
* The vocabulary storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, VocabularyStorageInterface $vocabulary_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->vocabularyStorage = $vocabulary_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('taxonomy_vocabulary')
);
}
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
// @todo Remove the legacy code.
// Convert legacy vids option to machine name vocabularies.
if (!empty($this->options['vids'])) {
$vocabularies = taxonomy_vocabulary_get_names();
foreach ($this->options['vids'] as $vid) {
if (isset($vocabularies[$vid], $vocabularies[$vid]->machine_name)) {
$this->options['vocabularies'][$vocabularies[$vid]->machine_name] = $vocabularies[$vid]->machine_name;
}
}
}
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['vids'] = ['default' => []];
return $options;
}
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$vocabularies = $this->vocabularyStorage->loadMultiple();
$options = [];
foreach ($vocabularies as $voc) {
$options[$voc->id()] = $voc->label();
}
$form['vids'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Vocabularies'),
'#options' => $options,
'#default_value' => $this->options['vids'],
'#description' => $this->t('Choose which vocabularies you wish to relate. Remember that every term found will create a new record, so this relationship is best used on just one vocabulary that has only one term per node.'),
];
parent::buildOptionsForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitOptionsForm(&$form, FormStateInterface $form_state) {
// Transform the #type = checkboxes value to a numerically indexed array,
// because the config schema expects a sequence, not a mapping.
$vids = $form_state->getValue(['options', 'vids']);
$form_state->setValue(['options', 'vids'], array_values(array_filter($vids)));
}
/**
* Called to implement a relationship in a query.
*/
public function query() {
$this->ensureMyTable();
$def = $this->definition;
$def['table'] = 'taxonomy_term_field_data';
if (!array_filter($this->options['vids'])) {
$taxonomy_index = $this->query->addTable('taxonomy_index', $this->relationship);
$def['left_table'] = $taxonomy_index;
$def['left_field'] = 'tid';
$def['field'] = 'tid';
$def['type'] = empty($this->options['required']) ? 'LEFT' : 'INNER';
}
else {
// If vocabularies are supplied join a subselect instead
$def['left_table'] = $this->tableAlias;
$def['left_field'] = 'nid';
$def['field'] = 'nid';
$def['type'] = empty($this->options['required']) ? 'LEFT' : 'INNER';
$def['adjusted'] = TRUE;
$query = Database::getConnection()->select('taxonomy_term_field_data', 'td');
$query->addJoin($def['type'], 'taxonomy_index', 'tn', 'tn.tid = td.tid');
$query->condition('td.vid', array_filter($this->options['vids']), 'IN');
$query->addTag('taxonomy_term_access');
$query->fields('td');
$query->fields('tn', ['nid']);
$def['table formula'] = $query;
}
$join = \Drupal::service('plugin.manager.views.join')->createInstance('standard', $def);
// use a short alias for this:
$alias = $def['table'] . '_' . $this->table;
$this->alias = $this->query->addRelationship($alias, $join, 'taxonomy_term_field_data', $this->relationship);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
foreach ($this->options['vids'] as $vocabulary_id) {
if ($vocabulary = $this->vocabularyStorage->load($vocabulary_id)) {
$dependencies[$vocabulary->getConfigDependencyKey()][] = $vocabulary->getConfigDependencyName();
}
}
return $dependencies;
}
}
@@ -0,0 +1,55 @@
<?php
namespace Drupal\taxonomy\Plugin\views\wizard;
use Drupal\views\Plugin\views\wizard\WizardPluginBase;
/**
* Tests creating taxonomy views with the wizard.
*
* @ViewsWizard(
* id = "taxonomy_term",
* base_table = "taxonomy_term_field_data",
* title = @Translation("Taxonomy terms")
* )
*/
class TaxonomyTerm extends WizardPluginBase {
/**
* {@inheritdoc}
*/
protected function defaultDisplayOptions() {
$display_options = parent::defaultDisplayOptions();
// Add permission-based access control.
$display_options['access']['type'] = 'perm';
$display_options['access']['options']['perm'] = 'access content';
// Remove the default fields, since we are customizing them here.
unset($display_options['fields']);
/* Field: Taxonomy: Term */
$display_options['fields']['name']['id'] = 'name';
$display_options['fields']['name']['table'] = 'taxonomy_term_field_data';
$display_options['fields']['name']['field'] = 'name';
$display_options['fields']['name']['entity_type'] = 'taxonomy_term';
$display_options['fields']['name']['entity_field'] = 'name';
$display_options['fields']['name']['label'] = '';
$display_options['fields']['name']['alter']['alter_text'] = 0;
$display_options['fields']['name']['alter']['make_link'] = 0;
$display_options['fields']['name']['alter']['absolute'] = 0;
$display_options['fields']['name']['alter']['trim'] = 0;
$display_options['fields']['name']['alter']['word_boundary'] = 0;
$display_options['fields']['name']['alter']['ellipsis'] = 0;
$display_options['fields']['name']['alter']['strip_tags'] = 0;
$display_options['fields']['name']['alter']['html'] = 0;
$display_options['fields']['name']['hide_empty'] = 0;
$display_options['fields']['name']['empty_zero'] = 0;
$display_options['fields']['name']['type'] = 'string';
$display_options['fields']['name']['settings']['link_to_entity'] = 1;
$display_options['fields']['name']['plugin_id'] = 'term_name';
return $display_options;
}
}
@@ -0,0 +1,85 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\taxonomy\Entity\Vocabulary;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides dynamic permissions of the taxonomy module.
*
* @see taxonomy.permissions.yml
*/
class TaxonomyPermissions implements ContainerInjectionInterface {
use StringTranslationTrait;
use DeprecatedServicePropertyTrait;
/**
* {@inheritdoc}
*/
protected $deprecatedProperties = ['entityManager' => 'entity.manager'];
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a TaxonomyPermissions instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('entity_type.manager'));
}
/**
* Get taxonomy permissions.
*
* @return array
* Permissions array.
*/
public function permissions() {
$permissions = [];
foreach (Vocabulary::loadMultiple() as $vocabulary) {
$permissions += $this->buildPermissions($vocabulary);
}
return $permissions;
}
/**
* Builds a standard list of taxonomy term permissions for a given vocabulary.
*
* @param \Drupal\taxonomy\VocabularyInterface $vocabulary
* The vocabulary.
*
* @return array
* An array of permission names and descriptions.
*/
protected function buildPermissions(VocabularyInterface $vocabulary) {
$id = $vocabulary->id();
$args = ['%vocabulary' => $vocabulary->label()];
return [
"create terms in $id" => ['title' => $this->t('%vocabulary: Create terms', $args)],
"delete terms in $id" => ['title' => $this->t('%vocabulary: Delete terms', $args)],
"edit terms in $id" => ['title' => $this->t('%vocabulary: Edit terms', $args)],
];
}
}
@@ -0,0 +1,62 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the access control handler for the taxonomy term entity type.
*
* @see \Drupal\taxonomy\Entity\Term
*/
class TermAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
if ($account->hasPermission('administer taxonomy')) {
return AccessResult::allowed()->cachePerPermissions();
}
switch ($operation) {
case 'view':
$access_result = AccessResult::allowedIf($account->hasPermission('access content') && $entity->isPublished())
->cachePerPermissions()
->addCacheableDependency($entity);
if (!$access_result->isAllowed()) {
$access_result->setReason("The 'access content' permission is required and the taxonomy term must be published.");
}
return $access_result;
case 'update':
if ($account->hasPermission("edit terms in {$entity->bundle()}")) {
return AccessResult::allowed()->cachePerPermissions();
}
return AccessResult::neutral()->setReason("The following permissions are required: 'edit terms in {$entity->bundle()}' OR 'administer taxonomy'.");
case 'delete':
if ($account->hasPermission("delete terms in {$entity->bundle()}")) {
return AccessResult::allowed()->cachePerPermissions();
}
return AccessResult::neutral()->setReason("The following permissions are required: 'delete terms in {$entity->bundle()}' OR 'administer taxonomy'.");
default:
// No opinion.
return AccessResult::neutral()->cachePerPermissions();
}
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
return AccessResult::allowedIfHasPermissions($account, ["create terms in $entity_bundle", 'administer taxonomy'], 'OR');
}
}
@@ -0,0 +1,103 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Provides a custom taxonomy breadcrumb builder that uses the term hierarchy.
*/
class TermBreadcrumbBuilder implements BreadcrumbBuilderInterface {
use StringTranslationTrait;
use DeprecatedServicePropertyTrait;
/**
* {@inheritdoc}
*/
protected $deprecatedProperties = ['entityManager' => 'entity.manager'];
/**
* The entity repository manager.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The taxonomy storage.
*
* @var \Drupal\taxonomy\TermStorageInterface
*/
protected $termStorage;
/**
* Constructs the TermBreadcrumbBuilder.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository = NULL) {
$this->entityTypeManager = $entity_type_manager;
$this->termStorage = $entity_type_manager->getStorage('taxonomy_term');
if (!$entity_repository) {
@trigger_error('The entity.repository service must be passed to TermBreadcrumbBuilder::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
$entity_repository = \Drupal::service('entity.repository');
}
$this->entityRepository = $entity_repository;
}
/**
* {@inheritdoc}
*/
public function applies(RouteMatchInterface $route_match) {
return $route_match->getRouteName() == 'entity.taxonomy_term.canonical'
&& $route_match->getParameter('taxonomy_term') instanceof TermInterface;
}
/**
* {@inheritdoc}
*/
public function build(RouteMatchInterface $route_match) {
$breadcrumb = new Breadcrumb();
$breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
$term = $route_match->getParameter('taxonomy_term');
// Breadcrumb needs to have terms cacheable metadata as a cacheable
// dependency even though it is not shown in the breadcrumb because e.g. its
// parent might have changed.
$breadcrumb->addCacheableDependency($term);
// @todo This overrides any other possible breadcrumb and is a pure
// hard-coded presumption. Make this behavior configurable per
// vocabulary or term.
$parents = $this->termStorage->loadAllParents($term->id());
// Remove current term being accessed.
array_shift($parents);
foreach (array_reverse($parents) as $term) {
$term = $this->entityRepository->getTranslationFromContext($term);
$breadcrumb->addCacheableDependency($term);
$breadcrumb->addLink(Link::createFromRoute($term->getName(), 'entity.taxonomy_term.canonical', ['taxonomy_term' => $term->id()]));
}
// This breadcrumb builder is based on a route parameter, and hence it
// depends on the 'route' cache context.
$breadcrumb->addCacheContexts(['route']);
return $breadcrumb;
}
}
+182
View File
@@ -0,0 +1,182 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Base for handler for taxonomy term edit forms.
*
* @internal
*/
class TermForm extends ContentEntityForm {
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$term = $this->entity;
$vocab_storage = $this->entityTypeManager->getStorage('taxonomy_vocabulary');
/** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */
$taxonomy_storage = $this->entityTypeManager->getStorage('taxonomy_term');
$vocabulary = $vocab_storage->load($term->bundle());
$parent = array_keys($taxonomy_storage->loadParents($term->id()));
$form_state->set(['taxonomy', 'parent'], $parent);
$form_state->set(['taxonomy', 'vocabulary'], $vocabulary);
$form['relations'] = [
'#type' => 'details',
'#title' => $this->t('Relations'),
'#open' => $taxonomy_storage->getVocabularyHierarchyType($vocabulary->id()) == VocabularyInterface::HIERARCHY_MULTIPLE,
'#weight' => 10,
];
// \Drupal\taxonomy\TermStorageInterface::loadTree() and
// \Drupal\taxonomy\TermStorageInterface::loadParents() may contain large
// numbers of items so we check for taxonomy.settings:override_selector
// before loading the full vocabulary. Contrib modules can then intercept
// before hook_form_alter to provide scalable alternatives.
if (!$this->config('taxonomy.settings')->get('override_selector')) {
$exclude = [];
if (!$term->isNew()) {
$parent = array_keys($taxonomy_storage->loadParents($term->id()));
$children = $taxonomy_storage->loadTree($vocabulary->id(), $term->id());
// A term can't be the child of itself, nor of its children.
foreach ($children as $child) {
$exclude[] = $child->tid;
}
$exclude[] = $term->id();
}
$tree = $taxonomy_storage->loadTree($vocabulary->id());
$options = ['<' . $this->t('root') . '>'];
if (empty($parent)) {
$parent = [0];
}
foreach ($tree as $item) {
if (!in_array($item->tid, $exclude)) {
$options[$item->tid] = str_repeat('-', $item->depth) . $item->name;
}
}
$form['relations']['parent'] = [
'#type' => 'select',
'#title' => $this->t('Parent terms'),
'#options' => $options,
'#default_value' => $parent,
'#multiple' => TRUE,
];
}
$form['relations']['weight'] = [
'#type' => 'textfield',
'#title' => $this->t('Weight'),
'#size' => 6,
'#default_value' => $term->getWeight(),
'#description' => $this->t('Terms are displayed in ascending order by weight.'),
'#required' => TRUE,
];
$form['vid'] = [
'#type' => 'value',
'#value' => $vocabulary->id(),
];
$form['tid'] = [
'#type' => 'value',
'#value' => $term->id(),
];
return parent::form($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
// Ensure numeric values.
if ($form_state->hasValue('weight') && !is_numeric($form_state->getValue('weight'))) {
$form_state->setErrorByName('weight', $this->t('Weight value must be numeric.'));
}
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
$term = parent::buildEntity($form, $form_state);
// Prevent leading and trailing spaces in term names.
$term->setName(trim($term->getName()));
// Assign parents with proper delta values starting from 0.
$term->parent = array_values($form_state->getValue('parent'));
return $term;
}
/**
* {@inheritdoc}
*/
protected function getEditedFieldNames(FormStateInterface $form_state) {
return array_merge(['parent', 'weight'], parent::getEditedFieldNames($form_state));
}
/**
* {@inheritdoc}
*/
protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
// Manually flag violations of fields not handled by the form display. This
// is necessary as entity form displays only flag violations for fields
// contained in the display.
// @see ::form()
foreach ($violations->getByField('parent') as $violation) {
$form_state->setErrorByName('parent', $violation->getMessage());
}
foreach ($violations->getByField('weight') as $violation) {
$form_state->setErrorByName('weight', $violation->getMessage());
}
parent::flagViolations($violations, $form, $form_state);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$term = $this->entity;
$result = $term->save();
$edit_link = $term->toLink($this->t('Edit'), 'edit-form')->toString();
$view_link = $term->toLink()->toString();
switch ($result) {
case SAVED_NEW:
$this->messenger()->addStatus($this->t('Created new term %term.', ['%term' => $view_link]));
$this->logger('taxonomy')->notice('Created new term %term.', ['%term' => $term->getName(), 'link' => $edit_link]);
break;
case SAVED_UPDATED:
$this->messenger()->addStatus($this->t('Updated term %term.', ['%term' => $view_link]));
$this->logger('taxonomy')->notice('Updated term %term.', ['%term' => $term->getName(), 'link' => $edit_link]);
break;
}
$current_parent_count = count($form_state->getValue('parent'));
// Root doesn't count if it's the only parent.
if ($current_parent_count == 1 && $form_state->hasValue(['parent', 0])) {
$form_state->setValue('parent', []);
}
$form_state->setValue('tid', $term->id());
$form_state->set('tid', $term->id());
}
}
@@ -0,0 +1,98 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\RevisionLogInterface;
/**
* Provides an interface defining a taxonomy term entity.
*/
interface TermInterface extends ContentEntityInterface, EntityChangedInterface, EntityPublishedInterface, RevisionLogInterface {
/**
* Gets the term description.
*
* @return string
* The term description.
*/
public function getDescription();
/**
* Sets the term description.
*
* @param string $description
* The term description.
*
* @return $this
*/
public function setDescription($description);
/**
* Gets the text format name for the term description.
*
* @return string
* The text format name.
*/
public function getFormat();
/**
* Sets the text format name for the term description.
*
* @param string $format
* The text format name.
*
* @return $this
*/
public function setFormat($format);
/**
* Gets the term name.
*
* @return string
* The term name.
*/
public function getName();
/**
* Sets the term name.
*
* @param string $name
* The term name.
*
* @return $this
*/
public function setName($name);
/**
* Gets the term weight.
*
* @return int
* The term weight.
*/
public function getWeight();
/**
* Sets the term weight.
*
* @param int $weight
* The term weight.
*
* @return $this
*/
public function setWeight($weight);
/**
* Gets the ID of the vocabulary that owns the term.
*
* @return string
* The vocabulary ID.
*
* @deprecated in drupal:8.?.? and is removed from drupal:9.0.0. Use
* TermInterface::bundle() instead.
*/
public function getVocabularyId();
}
@@ -0,0 +1,468 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Entity\Sql\TableMappingInterface;
/**
* Defines a Controller class for taxonomy terms.
*/
class TermStorage extends SqlContentEntityStorage implements TermStorageInterface {
/**
* Array of term parents keyed by vocabulary ID and child term ID.
*
* @var array
*/
protected $treeParents = [];
/**
* Array of term ancestors keyed by vocabulary ID and parent term ID.
*
* @var array
*/
protected $treeChildren = [];
/**
* Array of terms in a tree keyed by vocabulary ID and term ID.
*
* @var array
*/
protected $treeTerms = [];
/**
* Array of loaded trees keyed by a cache id matching tree arguments.
*
* @var array
*/
protected $trees = [];
/**
* Array of all loaded term ancestry keyed by ancestor term ID, keyed by term
* ID.
*
* @var \Drupal\taxonomy\TermInterface[][]
*/
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}
*
* @param array $values
* An array of values to set, keyed by property name. A value for the
* vocabulary ID ('vid') is required.
*/
public function create(array $values = []) {
// Save new terms with no parents by default.
if (empty($values['parent'])) {
$values['parent'] = [0];
}
$entity = parent::create($values);
return $entity;
}
/**
* {@inheritdoc}
*/
public function resetCache(array $ids = NULL) {
drupal_static_reset('taxonomy_term_count_nodes');
$this->ancestors = [];
$this->treeChildren = [];
$this->treeParents = [];
$this->treeTerms = [];
$this->trees = [];
$this->vocabularyHierarchyType = [];
parent::resetCache($ids);
}
/**
* {@inheritdoc}
*/
public function deleteTermHierarchy($tids) {}
/**
* {@inheritdoc}
*/
public function updateTermHierarchy(EntityInterface $term) {}
/**
* {@inheritdoc}
*/
public function loadParents($tid) {
$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 <root> 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
* <root> 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 <root> parent.
$parents[0] = NULL;
continue;
}
$ids[] = $item->target_id;
}
// @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) {
/** @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;
}
}
}
}
return $this->ancestors[$term->id()];
}
/**
* {@inheritdoc}
*/
public function loadChildren($tid, $vid = NULL) {
/** @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());
}
/**
* {@inheritdoc}
*/
public function loadTree($vid, $parent = 0, $max_depth = NULL, $load_entities = FALSE) {
$cache_key = implode(':', func_get_args());
if (!isset($this->trees[$cache_key])) {
// We cache trees, so it's not CPU-intensive to call on a term and its
// children, too.
if (!isset($this->treeChildren[$vid])) {
$this->treeChildren[$vid] = [];
$this->treeParents[$vid] = [];
$this->treeTerms[$vid] = [];
$query = $this->database->select($this->getDataTable(), 't');
$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')
->condition('t.vid', $vid)
->condition('t.default_langcode', 1)
->orderBy('t.weight')
->orderBy('t.name')
->execute();
foreach ($result as $term) {
$this->treeChildren[$vid][$term->parent][] = $term->tid;
$this->treeParents[$vid][$term->tid][] = $term->parent;
$this->treeTerms[$vid][$term->tid] = $term;
}
}
// Load full entities, if necessary. The entity controller statically
// caches the results.
$term_entities = [];
if ($load_entities) {
$term_entities = $this->loadMultiple(array_keys($this->treeTerms[$vid]));
}
$max_depth = (!isset($max_depth)) ? count($this->treeChildren[$vid]) : $max_depth;
$tree = [];
// Keeps track of the parents we have to process, the last entry is used
// for the next processing step.
$process_parents = [];
$process_parents[] = $parent;
// Loops over the parent terms and adds its children to the tree array.
// Uses a loop instead of a recursion, because it's more efficient.
while (count($process_parents)) {
$parent = array_pop($process_parents);
// The number of parents determines the current depth.
$depth = count($process_parents);
if ($max_depth > $depth && !empty($this->treeChildren[$vid][$parent])) {
$has_children = FALSE;
$child = current($this->treeChildren[$vid][$parent]);
do {
if (empty($child)) {
break;
}
$term = $load_entities ? $term_entities[$child] : $this->treeTerms[$vid][$child];
if (isset($this->treeParents[$vid][$load_entities ? $term->id() : $term->tid])) {
// Clone the term so that the depth attribute remains correct
// in the event of multiple parents.
$term = clone $term;
}
$term->depth = $depth;
if (!$load_entities) {
unset($term->parent);
}
$tid = $load_entities ? $term->id() : $term->tid;
$term->parents = $this->treeParents[$vid][$tid];
$tree[] = $term;
if (!empty($this->treeChildren[$vid][$tid])) {
$has_children = TRUE;
// We have to continue with this parent later.
$process_parents[] = $parent;
// Use the current term as parent for the next iteration.
$process_parents[] = $tid;
// Reset pointers for child lists because we step in there more
// often with multi parents.
reset($this->treeChildren[$vid][$tid]);
// Move pointer so that we get the correct term the next time.
next($this->treeChildren[$vid][$parent]);
break;
}
} while ($child = next($this->treeChildren[$vid][$parent]));
if (!$has_children) {
// We processed all terms in this hierarchy-level, reset pointer
// so that this function works the next time it gets called.
reset($this->treeChildren[$vid][$parent]);
}
}
}
$this->trees[$cache_key] = $tree;
}
return $this->trees[$cache_key];
}
/**
* {@inheritdoc}
*/
public function nodeCount($vid) {
$query = $this->database->select('taxonomy_index', 'ti');
$query->addExpression('COUNT(DISTINCT ti.nid)');
$query->leftJoin($this->getBaseTable(), 'td', 'ti.tid = td.tid');
$query->condition('td.vid', $vid);
$query->addTag('vocabulary_node_count');
return $query->execute()->fetchField();
}
/**
* {@inheritdoc}
*/
public function resetWeights($vid) {
$this->database->update($this->getDataTable())
->fields(['weight' => 0])
->condition('vid', $vid)
->execute();
}
/**
* {@inheritdoc}
*/
public function getNodeTerms(array $nids, array $vocabs = [], $langcode = NULL) {
$query = $this->database->select($this->getDataTable(), 'td');
$query->innerJoin('taxonomy_index', 'tn', 'td.tid = tn.tid');
$query->fields('td', ['tid']);
$query->addField('tn', 'nid', 'node_nid');
$query->orderby('td.weight');
$query->orderby('td.name');
$query->condition('tn.nid', $nids, 'IN');
$query->addTag('taxonomy_term_access');
if (!empty($vocabs)) {
$query->condition('td.vid', $vocabs, 'IN');
}
if (!empty($langcode)) {
$query->condition('td.langcode', $langcode);
}
$results = [];
$all_tids = [];
foreach ($query->execute() as $term_record) {
$results[$term_record->node_nid][] = $term_record->tid;
$all_tids[] = $term_record->tid;
}
$all_terms = $this->loadMultiple($all_tids);
$terms = [];
foreach ($results as $nid => $tids) {
foreach ($tids as $tid) {
$terms[$nid][$tid] = $all_terms[$tid];
}
}
return $terms;
}
/**
* {@inheritdoc}
*/
public function getTermIdsWithPendingRevisions() {
$table_mapping = $this->getTableMapping();
$id_field = $table_mapping->getColumnNames($this->entityType->getKey('id'))['value'];
$revision_field = $table_mapping->getColumnNames($this->entityType->getKey('revision'))['value'];
$rta_field = $table_mapping->getColumnNames($this->entityType->getKey('revision_translation_affected'))['value'];
$langcode_field = $table_mapping->getColumnNames($this->entityType->getKey('langcode'))['value'];
$revision_default_field = $table_mapping->getColumnNames($this->entityType->getRevisionMetadataKey('revision_default'))['value'];
$query = $this->database->select($this->getRevisionDataTable(), 'tfr');
$query->fields('tfr', [$id_field]);
$query->addExpression("MAX(tfr.$revision_field)", $revision_field);
$query->join($this->getRevisionTable(), 'tr', "tfr.$revision_field = tr.$revision_field AND tr.$revision_default_field = 0");
$inner_select = $this->database->select($this->getRevisionDataTable(), 't');
$inner_select->condition("t.$rta_field", '1');
$inner_select->fields('t', [$id_field, $langcode_field]);
$inner_select->addExpression("MAX(t.$revision_field)", $revision_field);
$inner_select
->groupBy("t.$id_field")
->groupBy("t.$langcode_field");
$query->join($inner_select, 'mr', "tfr.$revision_field = mr.$revision_field AND tfr.$langcode_field = mr.$langcode_field");
$query->groupBy("tfr.$id_field");
return $query->execute()->fetchAllKeyed(1, 0);
}
/**
* {@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->entityFieldManager->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'], $vars['vocabularyHierarchyType']);
return $vars;
}
/**
* {@inheritdoc}
*/
public function __wakeup() {
parent::__wakeup();
// Initialize static caches.
$this->ancestors = [];
$this->treeChildren = [];
$this->treeParents = [];
$this->treeTerms = [];
$this->trees = [];
$this->vocabularyHierarchyType = [];
}
}
@@ -0,0 +1,155 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
/**
* Defines an interface for taxonomy_term entity storage classes.
*/
interface TermStorageInterface extends ContentEntityStorageInterface {
/**
* Removed reference to terms from term_hierarchy.
*
* @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);
/**
* Updates terms hierarchy information with the hierarchy trail of it.
*
* @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);
/**
* Finds all parents of a given term ID.
*
* @param int $tid
* Term ID to retrieve parents for.
*
* @return \Drupal\taxonomy\TermInterface[]
* An array of term objects which are the parents of the term $tid.
*/
public function loadParents($tid);
/**
* Finds all ancestors of a given term ID.
*
* @param int $tid
* Term ID to retrieve ancestors for.
*
* @return \Drupal\taxonomy\TermInterface[]
* An array of term objects which are the ancestors of the term $tid.
*/
public function loadAllParents($tid);
/**
* Finds all children of a term ID.
*
* @param int $tid
* Term ID to retrieve children for.
* @param string $vid
* An optional vocabulary ID to restrict the child search.
*
* @return \Drupal\taxonomy\TermInterface[]
* An array of term objects that are the children of the term $tid.
*/
public function loadChildren($tid, $vid = NULL);
/**
* Finds all terms in a given vocabulary ID.
*
* @param string $vid
* Vocabulary ID to retrieve terms for.
* @param int $parent
* The term ID under which to generate the tree. If 0, generate the tree
* for the entire vocabulary.
* @param int $max_depth
* The number of levels of the tree to return. Leave NULL to return all
* levels.
* @param bool $load_entities
* If TRUE, a full entity load will occur on the term objects. Otherwise
* they are partial objects queried directly from the {taxonomy_term_data}
* table to save execution time and memory consumption when listing large
* numbers of terms. Defaults to FALSE.
*
* @return object[]|\Drupal\taxonomy\TermInterface[]
* An array of term objects that are the children of the vocabulary $vid.
*/
public function loadTree($vid, $parent = 0, $max_depth = NULL, $load_entities = FALSE);
/**
* Count the number of nodes in a given vocabulary ID.
*
* @param string $vid
* Vocabulary ID to retrieve terms for.
*
* @return int
* A count of the nodes in a given vocabulary ID.
*/
public function nodeCount($vid);
/**
* Reset the weights for a given vocabulary ID.
*
* @param string $vid
* Vocabulary ID to retrieve terms for.
*/
public function resetWeights($vid);
/**
* Returns all terms used to tag some given nodes.
*
* @param array $nids
* Node IDs to retrieve terms for.
* @param array $vocabs
* (optional) A vocabularies array to restrict the term search. Defaults to
* empty array.
* @param string $langcode
* (optional) A language code to restrict the term search. Defaults to NULL.
*
* @return array
* An array of nids and the term entities they were tagged with.
*/
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);
/**
* Gets a list of term IDs with pending revisions.
*
* @return int[]
* An array of term IDs which have pending revisions, keyed by their
* revision IDs.
*
* @internal
*/
public function getTermIdsWithPendingRevisions();
}
@@ -0,0 +1,136 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Defines the term schema handler.
*/
class TermStorageSchema extends SqlContentEntityStorageSchema {
/**
* {@inheritdoc}
*/
protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
$schema = parent::getEntitySchema($entity_type, $reset);
if ($data_table = $this->storage->getDataTable()) {
$schema[$data_table]['indexes'] += [
'taxonomy_term__tree' => ['vid', 'weight', 'name'],
'taxonomy_term__vid_name' => ['vid', 'name'],
];
}
$schema['taxonomy_index'] = [
'description' => 'Maintains denormalized information about node/term relationships.',
'fields' => [
'nid' => [
'description' => 'The {node}.nid this record tracks.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'tid' => [
'description' => 'The term ID.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'status' => [
'description' => 'Boolean indicating whether the node is published (visible to non-administrators).',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
],
'sticky' => [
'description' => 'Boolean indicating whether the node is sticky.',
'type' => 'int',
'not null' => FALSE,
'default' => 0,
'size' => 'tiny',
],
'created' => [
'description' => 'The Unix timestamp when the node was created.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
],
],
'primary key' => ['nid', 'tid'],
'indexes' => [
'term_node' => ['tid', 'status', 'sticky', 'created'],
],
'foreign keys' => [
'tracked_node' => [
'table' => 'node',
'columns' => ['nid' => 'nid'],
],
'term' => [
'table' => 'taxonomy_term_data',
'columns' => ['tid' => 'tid'],
],
],
];
return $schema;
}
/**
* {@inheritdoc}
*/
protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
$schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
$field_name = $storage_definition->getName();
if ($table_name == 'taxonomy_term_field_data') {
// Remove unneeded indexes.
unset($schema['indexes']['taxonomy_term_field__vid__target_id']);
unset($schema['indexes']['taxonomy_term_field__description__format']);
switch ($field_name) {
case 'weight':
// Improves the performance of the taxonomy_term__tree index defined
// in getEntitySchema().
$schema['fields'][$field_name]['not null'] = TRUE;
break;
case 'name':
$this->addSharedTableFieldIndex($storage_definition, $schema, TRUE);
break;
}
}
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;
}
}
@@ -0,0 +1,53 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\EntityInterface;
use Drupal\content_translation\ContentTranslationHandler;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines the translation handler for terms.
*/
class TermTranslationHandler extends ContentTranslationHandler {
/**
* {@inheritdoc}
*/
public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity) {
parent::entityFormAlter($form, $form_state, $entity);
$form['content_translation']['status']['#access'] = !isset($form['content_translation']);
$form['actions']['submit']['#submit'][] = [$this, 'entityFormSave'];
}
/**
* Form submission handler for TermTranslationHandler::entityFormAlter().
*
* This handles the save action.
*
* @see \Drupal\Core\Entity\EntityForm::build()
*/
public function entityFormSave(array $form, FormStateInterface $form_state) {
if ($this->getSourceLangcode($form_state)) {
$entity = $form_state->getFormObject()->getEntity();
// We need a redirect here, otherwise we would get an access denied page,
// since the current URL would be preserved and we would try to add a
// translation for a language that already has a translation.
$form_state->setRedirectUrl($entity->toUrl('edit-form'));
}
}
/**
* {@inheritdoc}
*/
public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, FormStateInterface $form_state) {
if ($form_state->hasValue('content_translation')) {
$translation = &$form_state->getValue('content_translation');
$translation['status'] = $entity->isPublished();
}
parent::entityFormEntityBuild($entity_type, $entity, $form, $form_state);
}
}
@@ -0,0 +1,18 @@
<?php
namespace Drupal\taxonomy;
@trigger_error(__NAMESPACE__ . '\TermViewBuilder is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityViewBuilder instead. See https://www.drupal.org/node/2924233.', E_USER_DEPRECATED);
use Drupal\Core\Entity\EntityViewBuilder;
/**
* View builder handler for taxonomy terms.
*
* @deprecated in drupal:8.5.0 and is removed from drupal:9.0.0.
* Use \Drupal\Core\Entity\EntityViewBuilder instead.
*
* @see \Drupal\Core\Entity\EntityViewBuilder
* @see https://www.drupal.org/node/2924233
*/
class TermViewBuilder extends EntityViewBuilder {}
@@ -0,0 +1,239 @@
<?php
namespace Drupal\taxonomy;
use Drupal\views\EntityViewsData;
/**
* Provides the views data for the taxonomy entity type.
*/
class TermViewsData extends EntityViewsData {
/**
* {@inheritdoc}
*/
public function getViewsData() {
$data = parent::getViewsData();
$data['taxonomy_term_field_data']['table']['base']['help'] = $this->t('Taxonomy terms are attached to nodes.');
$data['taxonomy_term_field_data']['table']['base']['access query tag'] = 'taxonomy_term_access';
$data['taxonomy_term_field_data']['table']['wizard_id'] = 'taxonomy_term';
$data['taxonomy_term_field_data']['table']['join'] = [
// This is provided for the many_to_one argument.
'taxonomy_index' => [
'field' => 'tid',
'left_field' => 'tid',
],
];
$data['taxonomy_term_field_data']['tid']['help'] = $this->t('The tid of a taxonomy term.');
$data['taxonomy_term_field_data']['tid']['argument']['id'] = 'taxonomy';
$data['taxonomy_term_field_data']['tid']['argument']['name field'] = 'name';
$data['taxonomy_term_field_data']['tid']['argument']['zero is null'] = TRUE;
$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__parent';
$data['taxonomy_term_field_data']['tid']['filter']['numeric'] = TRUE;
$data['taxonomy_term_field_data']['tid_raw'] = [
'title' => $this->t('Term ID'),
'help' => $this->t('The tid of a taxonomy term.'),
'real field' => 'tid',
'filter' => [
'id' => 'numeric',
'allow empty' => TRUE,
],
];
$data['taxonomy_term_field_data']['tid_representative'] = [
'relationship' => [
'title' => $this->t('Representative node'),
'label' => $this->t('Representative node'),
'help' => $this->t('Obtains a single representative node for each term, according to a chosen sort criterion.'),
'id' => 'groupwise_max',
'relationship field' => 'tid',
'outer field' => 'taxonomy_term_field_data.tid',
'argument table' => 'taxonomy_term_field_data',
'argument field' => 'tid',
'base' => 'node_field_data',
'field' => 'nid',
'relationship' => 'node_field_data:term_node_tid',
],
];
$data['taxonomy_term_field_data']['vid']['help'] = $this->t('Filter the results of "Taxonomy: Term" to a particular vocabulary.');
$data['taxonomy_term_field_data']['vid']['field']['help'] = t('The vocabulary name.');
$data['taxonomy_term_field_data']['vid']['argument']['id'] = 'vocabulary_vid';
unset($data['taxonomy_term_field_data']['vid']['sort']);
$data['taxonomy_term_field_data']['name']['field']['id'] = 'term_name';
$data['taxonomy_term_field_data']['name']['argument']['many to one'] = TRUE;
$data['taxonomy_term_field_data']['name']['argument']['empty field name'] = $this->t('Uncategorized');
$data['taxonomy_term_field_data']['description__value']['field']['click sortable'] = FALSE;
$data['taxonomy_term_field_data']['changed']['title'] = $this->t('Updated date');
$data['taxonomy_term_field_data']['changed']['help'] = $this->t('The date the term was last updated.');
$data['taxonomy_term_field_data']['changed_fulldate'] = [
'title' => $this->t('Updated date'),
'help' => $this->t('Date in the form of CCYYMMDD.'),
'argument' => [
'field' => 'changed',
'id' => 'date_fulldate',
],
];
$data['taxonomy_term_field_data']['changed_year_month'] = [
'title' => $this->t('Updated year + month'),
'help' => $this->t('Date in the form of YYYYMM.'),
'argument' => [
'field' => 'changed',
'id' => 'date_year_month',
],
];
$data['taxonomy_term_field_data']['changed_year'] = [
'title' => $this->t('Updated year'),
'help' => $this->t('Date in the form of YYYY.'),
'argument' => [
'field' => 'changed',
'id' => 'date_year',
],
];
$data['taxonomy_term_field_data']['changed_month'] = [
'title' => $this->t('Updated month'),
'help' => $this->t('Date in the form of MM (01 - 12).'),
'argument' => [
'field' => 'changed',
'id' => 'date_month',
],
];
$data['taxonomy_term_field_data']['changed_day'] = [
'title' => $this->t('Updated day'),
'help' => $this->t('Date in the form of DD (01 - 31).'),
'argument' => [
'field' => 'changed',
'id' => 'date_day',
],
];
$data['taxonomy_term_field_data']['changed_week'] = [
'title' => $this->t('Updated week'),
'help' => $this->t('Date in the form of WW (01 - 53).'),
'argument' => [
'field' => 'changed',
'id' => 'date_week',
],
];
$data['taxonomy_index']['table']['group'] = $this->t('Taxonomy term');
$data['taxonomy_index']['table']['join'] = [
'taxonomy_term_field_data' => [
// links directly to taxonomy_term_field_data via tid
'left_field' => 'tid',
'field' => 'tid',
],
'node_field_data' => [
// links directly to node via nid
'left_field' => 'nid',
'field' => 'nid',
],
'taxonomy_term__parent' => [
'left_field' => 'entity_id',
'field' => 'tid',
],
];
$data['taxonomy_index']['nid'] = [
'title' => $this->t('Content with term'),
'help' => $this->t('Relate all content tagged with a term.'),
'relationship' => [
'id' => 'standard',
'base' => 'node_field_data',
'base field' => 'nid',
'label' => $this->t('node'),
'skip base' => 'node_field_data',
],
];
// @todo This stuff needs to move to a node field since really it's all
// about nodes.
$data['taxonomy_index']['tid'] = [
'group' => $this->t('Content'),
'title' => $this->t('Has taxonomy term ID'),
'help' => $this->t('Display content if it has the selected taxonomy terms.'),
'argument' => [
'id' => 'taxonomy_index_tid',
'name table' => 'taxonomy_term_field_data',
'name field' => 'name',
'empty field name' => $this->t('Uncategorized'),
'numeric' => TRUE,
'skip base' => 'taxonomy_term_field_data',
],
'filter' => [
'title' => $this->t('Has taxonomy term'),
'id' => 'taxonomy_index_tid',
'hierarchy table' => 'taxonomy_term__parent',
'numeric' => TRUE,
'skip base' => 'taxonomy_term_field_data',
'allow empty' => TRUE,
],
];
$data['taxonomy_index']['status'] = [
'title' => $this->t('Publish status'),
'help' => $this->t('Whether or not the content related to a term is published.'),
'filter' => [
'id' => 'boolean',
'label' => $this->t('Published status'),
'type' => 'yes-no',
],
];
$data['taxonomy_index']['sticky'] = [
'title' => $this->t('Sticky status'),
'help' => $this->t('Whether or not the content related to a term is sticky.'),
'filter' => [
'id' => 'boolean',
'label' => $this->t('Sticky status'),
'type' => 'yes-no',
],
'sort' => [
'id' => 'standard',
'help' => $this->t('Whether or not the content related to a term is sticky. To list sticky content first, set this to descending.'),
],
];
$data['taxonomy_index']['created'] = [
'title' => $this->t('Post date'),
'help' => $this->t('The date the content related to a term was posted.'),
'sort' => [
'id' => 'date',
],
'filter' => [
'id' => 'date',
],
];
// 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__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;
}
}
@@ -0,0 +1,42 @@
<?php
namespace Drupal\taxonomy\Tests;
@trigger_error(__NAMESPACE__ . '\TaxonomyTestBase is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Instead, use \Drupal\Tests\taxonomy\Functional\TaxonomyTestBase', E_USER_DEPRECATED);
use Drupal\simpletest\WebTestBase;
use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
/**
* Provides common helper methods for Taxonomy module tests.
*
* @deprecated in drupal:8.?.? and is removed from drupal:9.0.0.
* Use \Drupal\Tests\taxonomy\Functional\TaxonomyTestBase instead.
*/
abstract class TaxonomyTestBase extends WebTestBase {
use TaxonomyTestTrait;
use EntityReferenceTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['taxonomy', 'block'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
// Create Basic page and Article node types.
if ($this->profile != 'standard') {
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
}
}
}
@@ -0,0 +1,64 @@
<?php
namespace Drupal\taxonomy\Tests;
@trigger_error(__NAMESPACE__ . '\TaxonomyTestTrait is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Instead, use \Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait instead', E_USER_DEPRECATED);
use Drupal\Core\Language\LanguageInterface;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\taxonomy\Entity\Term;
/**
* Provides common helper methods for Taxonomy module tests.
*
* @deprecated in drupal:8.4.0 and is removed from drupal:9.0.0.
* Use \Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait instead.
*/
trait TaxonomyTestTrait {
/**
* Returns a new vocabulary with random properties.
*/
public function createVocabulary() {
// Create a vocabulary.
$vocabulary = Vocabulary::create([
'name' => $this->randomMachineName(),
'description' => $this->randomMachineName(),
'vid' => mb_strtolower($this->randomMachineName()),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'weight' => mt_rand(0, 10),
]);
$vocabulary->save();
return $vocabulary;
}
/**
* Returns a new term with random properties in vocabulary $vid.
*
* @param \Drupal\taxonomy\Entity\Vocabulary $vocabulary
* The vocabulary object.
* @param array $values
* (optional) An array of values to set, keyed by property name. If the
* entity type has bundles, the bundle key has to be specified.
*
* @return \Drupal\taxonomy\Entity\Term
* The new taxonomy term object.
*/
public function createTerm(Vocabulary $vocabulary, $values = []) {
$filter_formats = filter_formats();
$format = array_pop($filter_formats);
$term = Term::create($values + [
'name' => $this->randomMachineName(),
'description' => [
'value' => $this->randomMachineName(),
// Use the first available text format.
'format' => $format->id(),
],
'vid' => $vocabulary->id(),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
$term->save();
return $term;
}
}
@@ -0,0 +1,105 @@
<?php
namespace Drupal\taxonomy\Tests;
@trigger_error(__NAMESPACE__ . '\TaxonomyTranslationTestTrait is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Instead, use \Drupal\Tests\taxonomy\Functional\TaxonomyTranslationTestTrait', E_USER_DEPRECATED);
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
/**
* Provides common testing base for translated taxonomy terms.
*
* @deprecated in drupal:8.4.0 and is removed from drupal:9.0.0.
* Use \Drupal\Tests\taxonomy\Functional\TaxonomyTranslationTestTrait
*/
trait TaxonomyTranslationTestTrait {
use EntityReferenceTestTrait;
/**
* The vocabulary.
*
* @var \Drupal\taxonomy\Entity\Vocabulary
*/
protected $vocabulary;
/**
* The field name for our taxonomy term field.
*
* @var string
*/
protected $termFieldName = 'field_tag';
/**
* The langcode of the source language.
*
* @var string
*/
protected $baseLangcode = 'en';
/**
* Target langcode for translation.
*
* @var string
*/
protected $translateToLangcode = 'hu';
/**
* The node to check the translated value on.
*
* @var \Drupal\node\Entity\Node
*/
protected $node;
/**
* Adds additional languages.
*/
protected function setupLanguages() {
ConfigurableLanguage::createFromLangcode($this->translateToLangcode)->save();
$this->rebuildContainer();
}
/**
* Enables translations where it needed.
*/
protected function enableTranslation() {
// Enable translation for the current entity type and ensure the change is
// picked up.
\Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE);
\Drupal::service('content_translation.manager')->setEnabled('taxonomy_term', $this->vocabulary->id(), TRUE);
\Drupal::entityManager()->clearCachedDefinitions();
}
/**
* Adds term reference field for the article content type.
*/
protected function setUpTermReferenceField() {
$handler_settings = [
'target_bundles' => [
$this->vocabulary->id() => $this->vocabulary->id(),
],
'auto_create' => TRUE,
];
$this->createEntityReferenceField('node', 'article', $this->termFieldName, NULL, 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$field_storage = FieldStorageConfig::loadByName('node', $this->termFieldName);
$field_storage->setTranslatable(FALSE);
$field_storage->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('node', 'article')
->setComponent($this->termFieldName, [
'type' => 'entity_reference_autocomplete_tags',
])
->save();
$display_repository->getViewDisplay('node', 'article')
->setComponent($this->termFieldName, [
'type' => 'entity_reference_label',
])
->save();
}
}
@@ -0,0 +1,31 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the access control handler for the taxonomy vocabulary entity type.
*
* @see \Drupal\taxonomy\Entity\Vocabulary
*/
class VocabularyAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
switch ($operation) {
case 'access taxonomy overview':
case 'view':
return AccessResult::allowedIfHasPermissions($account, ['access taxonomy overview', 'administer taxonomy'], 'OR');
default:
return parent::checkAccess($entity, $operation, $account);
}
}
}
@@ -0,0 +1,158 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\BundleEntityFormBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\language\Entity\ContentLanguageSettings;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base form for vocabulary edit forms.
*
* @internal
*/
class VocabularyForm extends BundleEntityFormBase {
/**
* The vocabulary storage.
*
* @var \Drupal\taxonomy\VocabularyStorageInterface
*/
protected $vocabularyStorage;
/**
* Constructs a new vocabulary form.
*
* @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage
* The vocabulary storage.
*/
public function __construct(VocabularyStorageInterface $vocabulary_storage) {
$this->vocabularyStorage = $vocabulary_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('taxonomy_vocabulary')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$vocabulary = $this->entity;
if ($vocabulary->isNew()) {
$form['#title'] = $this->t('Add vocabulary');
}
else {
$form['#title'] = $this->t('Edit vocabulary');
}
$form['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Name'),
'#default_value' => $vocabulary->label(),
'#maxlength' => 255,
'#required' => TRUE,
];
$form['vid'] = [
'#type' => 'machine_name',
'#default_value' => $vocabulary->id(),
'#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
'#machine_name' => [
'exists' => [$this, 'exists'],
'source' => ['name'],
],
];
$form['description'] = [
'#type' => 'textfield',
'#title' => $this->t('Description'),
'#default_value' => $vocabulary->getDescription(),
];
// $form['langcode'] is not wrapped in an
// if ($this->moduleHandler->moduleExists('language')) check because the
// language_select form element works also without the language module being
// installed. https://www.drupal.org/node/1749954 documents the new element.
$form['langcode'] = [
'#type' => 'language_select',
'#title' => $this->t('Vocabulary language'),
'#languages' => LanguageInterface::STATE_ALL,
'#default_value' => $vocabulary->language()->getId(),
];
if ($this->moduleHandler->moduleExists('language')) {
$form['default_terms_language'] = [
'#type' => 'details',
'#title' => $this->t('Term language'),
'#open' => TRUE,
];
$form['default_terms_language']['default_language'] = [
'#type' => 'language_configuration',
'#entity_information' => [
'entity_type' => 'taxonomy_term',
'bundle' => $vocabulary->id(),
],
'#default_value' => ContentLanguageSettings::loadByEntityTypeBundle('taxonomy_term', $vocabulary->id()),
];
}
// Set the hierarchy to "multiple parents" by default. This simplifies the
// vocabulary form and standardizes the term form.
$form['hierarchy'] = [
'#type' => 'value',
'#value' => '0',
];
$form = parent::form($form, $form_state);
return $this->protectBundleIdElement($form);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$vocabulary = $this->entity;
// Prevent leading and trailing spaces in vocabulary names.
$vocabulary->set('name', trim($vocabulary->label()));
$status = $vocabulary->save();
$edit_link = $this->entity->toLink($this->t('Edit'), 'edit-form')->toString();
switch ($status) {
case SAVED_NEW:
$this->messenger()->addStatus($this->t('Created new vocabulary %name.', ['%name' => $vocabulary->label()]));
$this->logger('taxonomy')->notice('Created new vocabulary %name.', ['%name' => $vocabulary->label(), 'link' => $edit_link]);
$form_state->setRedirectUrl($vocabulary->toUrl('overview-form'));
break;
case SAVED_UPDATED:
$this->messenger()->addStatus($this->t('Updated vocabulary %name.', ['%name' => $vocabulary->label()]));
$this->logger('taxonomy')->notice('Updated vocabulary %name.', ['%name' => $vocabulary->label(), 'link' => $edit_link]);
$form_state->setRedirectUrl($vocabulary->toUrl('collection'));
break;
}
$form_state->setValue('vid', $vocabulary->id());
$form_state->set('vid', $vocabulary->id());
}
/**
* Determines if the vocabulary already exists.
*
* @param string $vid
* The vocabulary ID.
*
* @return bool
* TRUE if the vocabulary exists, FALSE otherwise.
*/
public function exists($vid) {
$action = $this->vocabularyStorage->load($vid);
return !empty($action);
}
}
@@ -0,0 +1,63 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining a taxonomy vocabulary entity.
*/
interface VocabularyInterface extends ConfigEntityInterface {
/**
* Denotes that no term in the vocabulary has a parent.
*/
const HIERARCHY_DISABLED = 0;
/**
* Denotes that one or more terms in the vocabulary has a single parent.
*/
const HIERARCHY_SINGLE = 1;
/**
* Denotes that one or more terms in the vocabulary have multiple parents.
*/
const HIERARCHY_MULTIPLE = 2;
/**
* Returns the vocabulary hierarchy.
*
* @return int
* The vocabulary hierarchy.
*
* @deprecated in drupal:8.7.0 and is removed from drupal:9.0.0. Use
* \Drupal\taxonomy\TermStorage::getVocabularyHierarchyType() instead.
*/
public function getHierarchy();
/**
* Sets the vocabulary hierarchy.
*
* @param int $hierarchy
* The hierarchy type of vocabulary.
* Possible values:
* - VocabularyInterface::HIERARCHY_DISABLED: No parents.
* - VocabularyInterface::HIERARCHY_SINGLE: Single parent.
* - VocabularyInterface::HIERARCHY_MULTIPLE: Multiple parents.
*
* @return $this
*
* @deprecated in drupal:8.7.0 and is removed from drupal:9.0.0. Reset
* the cache of the taxonomy_term storage handler instead.
*/
public function setHierarchy($hierarchy);
/**
* Returns the vocabulary description.
*
* @return string
* The vocabulary description.
*/
public function getDescription();
}
@@ -0,0 +1,211 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Config\Entity\DraggableListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class to build a listing of taxonomy vocabulary entities.
*
* @see \Drupal\taxonomy\Entity\Vocabulary
*/
class VocabularyListBuilder extends DraggableListBuilder {
/**
* {@inheritdoc}
*/
protected $entitiesKey = 'vocabularies';
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new VocabularyListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
*/
public function __construct(EntityTypeInterface $entity_type,
AccountInterface $current_user,
EntityTypeManagerInterface $entity_type_manager,
RendererInterface $renderer = NULL,
MessengerInterface $messenger) {
parent::__construct($entity_type, $entity_type_manager->getStorage($entity_type->id()));
$this->currentUser = $current_user;
$this->entityTypeManager = $entity_type_manager;
$this->renderer = $renderer;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('current_user'),
$container->get('entity_type.manager'),
$container->get('renderer'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'taxonomy_overview_vocabularies';
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
if (isset($operations['edit'])) {
$operations['edit']['title'] = t('Edit vocabulary');
}
if ($entity->access('access taxonomy overview')) {
$operations['list'] = [
'title' => t('List terms'),
'weight' => 0,
'url' => $entity->toUrl('overview-form'),
];
}
$taxonomy_term_access_control_handler = $this->entityTypeManager->getAccessControlHandler('taxonomy_term');
if ($taxonomy_term_access_control_handler->createAccess($entity->id())) {
$operations['add'] = [
'title' => t('Add terms'),
'weight' => 10,
'url' => Url::fromRoute('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $entity->id()]),
];
}
unset($operations['delete']);
return $operations;
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = t('Vocabulary name');
$header['description'] = t('Description');
if ($this->currentUser->hasPermission('administer vocabularies') && !empty($this->weightKey)) {
$header['weight'] = t('Weight');
}
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $entity->label();
$row['description']['data'] = ['#markup' => $entity->getDescription()];
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function render() {
$entities = $this->load();
// If there are not multiple vocabularies, disable dragging by unsetting the
// weight key.
if (count($entities) <= 1) {
unset($this->weightKey);
}
$build = parent::render();
// If the weight key was unset then the table is in the 'table' key,
// otherwise in vocabularies. The empty message is only needed if the table
// is possibly empty, so there is no need to support the vocabularies key
// here.
if (isset($build['table'])) {
$access_control_handler = $this->entityTypeManager->getAccessControlHandler('taxonomy_vocabulary');
$create_access = $access_control_handler->createAccess(NULL, NULL, [], TRUE);
$this->renderer->addCacheableDependency($build['table'], $create_access);
if ($create_access->isAllowed()) {
$build['table']['#empty'] = t('No vocabularies available. <a href=":link">Add vocabulary</a>.', [
':link' => Url::fromRoute('entity.taxonomy_vocabulary.add_form')->toString(),
]);
}
else {
$build['table']['#empty'] = t('No vocabularies available.');
}
}
return $build;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$form['vocabularies']['#attributes'] = ['id' => 'taxonomy'];
$form['actions']['submit']['#value'] = t('Save');
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$this->messenger->addStatus($this->t('The configuration options have been saved.'));
}
}
@@ -0,0 +1,32 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
/**
* Defines a storage handler class for taxonomy vocabularies.
*/
class VocabularyStorage extends ConfigEntityStorage implements VocabularyStorageInterface {
/**
* {@inheritdoc}
*/
public function resetCache(array $ids = NULL) {
drupal_static_reset('taxonomy_vocabulary_get_names');
parent::resetCache($ids);
}
/**
* {@inheritdoc}
*/
public function getToplevelTids($vids) {
$tids = \Drupal::entityQuery('taxonomy_term')
->condition('vid', $vids, 'IN')
->condition('parent.target_id', 0)
->execute();
return array_values($tids);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
/**
* Defines an interface for vocabulary entity storage classes.
*/
interface VocabularyStorageInterface extends ConfigEntityStorageInterface {
/**
* Gets top-level term IDs of vocabularies.
*
* @param array $vids
* Array of vocabulary IDs.
*
* @return array
* Array of top-level term IDs.
*/
public function getToplevelTids($vids);
}