Move all files to 2017/

This commit is contained in:
Oliver Davies 2025-09-29 22:25:17 +01:00
parent ac7370f67f
commit 2875863330
15717 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,9 @@
langcode: en
status: true
dependencies:
module:
- taxonomy
id: taxonomy_term.full
label: 'Taxonomy term page'
targetEntityType: taxonomy_term
cache: true

View file

@ -0,0 +1,3 @@
maintain_index_table: true
override_selector: false
terms_per_page_admin: 100

View file

@ -0,0 +1,311 @@
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.teaser
module:
- node
- taxonomy
- user
id: taxonomy_term
label: 'Taxonomy term'
module: taxonomy
description: 'Content belonging to a certain taxonomy term.'
tag: default
base_table: node_field_data
base_field: nid
core: '8'
display:
default:
id: default
display_title: Master
display_plugin: default
position: 0
display_options:
query:
type: views_query
options:
query_comment: ''
disable_sql_rewrite: false
distinct: false
replica: false
query_tags: { }
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: 0
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous:
next:
sorts:
sticky:
id: sticky
table: taxonomy_index
field: sticky
order: DESC
plugin_id: standard
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
created:
id: created
table: taxonomy_index
field: created
order: DESC
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
arguments:
tid:
id: tid
table: taxonomy_index
field: tid
relationship: none
group_type: group
admin_label: ''
default_action: 'not found'
exception:
value: ''
title_enable: false
title: All
title_enable: true
title: '{{ arguments.tid }}'
default_argument_type: fixed
default_argument_options:
argument: ''
default_argument_skip_url: false
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: true
validate:
type: 'entity:taxonomy_term'
fail: 'not found'
validate_options:
access: true
operation: view
multiple: 0
bundles: { }
break_phrase: false
add_table: false
require_value: false
reduce_duplicates: false
plugin_id: taxonomy_index_tid
filters:
langcode:
id: langcode
table: node_field_data
field: langcode
relationship: none
group_type: group
admin_label: ''
operator: in
value:
'***LANGUAGE_language_content***': '***LANGUAGE_language_content***'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: language
entity_type: node
entity_field: langcode
status:
id: status
table: taxonomy_index
field: status
relationship: none
group_type: group
admin_label: ''
operator: '='
value: '1'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: boolean
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: 'entity:node'
options:
view_mode: teaser
header:
entity_taxonomy_term:
id: entity_taxonomy_term
table: views
field: entity_taxonomy_term
relationship: none
group_type: group
admin_label: ''
empty: true
tokenize: true
target: '{{ raw_arguments.tid }}'
view_mode: full
bypass_access: false
plugin_id: entity
footer: { }
empty: { }
relationships: { }
fields: { }
display_extenders: { }
link_url: ''
link_display: page_1
cache_metadata:
contexts:
- 'languages:language_interface'
- url
- url.query_args
- 'user.node_grants:view'
- user.permissions
max-age: -1
tags: { }
feed_1:
id: feed_1
display_title: Feed
display_plugin: feed
position: 2
display_options:
query:
type: views_query
options: { }
pager:
type: some
options:
items_per_page: 10
offset: 0
path: taxonomy/term/%/feed
displays:
page_1: page_1
default: '0'
style:
type: rss
options:
description: ''
grouping: { }
uses_fields: false
row:
type: node_rss
options:
relationship: none
view_mode: default
display_extenders: { }
cache_metadata:
contexts:
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
max-age: -1
tags: { }
page_1:
id: page_1
display_title: Page
display_plugin: page
position: 1
display_options:
query:
type: views_query
options: { }
path: taxonomy/term/%
display_extenders: { }
cache_metadata:
contexts:
- 'languages:language_interface'
- url
- url.query_args
- 'user.node_grants:view'
- user.permissions
max-age: -1
tags: { }

View file

@ -0,0 +1,39 @@
# Schema for the configuration files of the Taxonomy module.
taxonomy.settings:
type: config_object
label: 'Taxonomy settings'
mapping:
maintain_index_table:
type: boolean
label: 'Maintain index table'
override_selector:
type: boolean
label: 'Override selector'
terms_per_page_admin:
type: integer
label: 'Number of terms per page'
taxonomy.vocabulary.*:
type: config_entity
label: 'Vocabulary'
mapping:
name:
type: label
label: 'Name'
vid:
type: string
label: 'Machine name'
description:
type: label
label: 'Description'
hierarchy:
type: integer
label: 'Hierarchy'
weight:
type: integer
label: 'Weight'
field.formatter.settings.entity_reference_rss_category:
type: mapping
label: 'Taxonomy format settings'

View file

@ -0,0 +1,162 @@
# Schema for the views plugins of the Taxonomy module.
views.argument.taxonomy_index_tid:
type: views.argument.many_to_one
label: 'Taxonomy term ID'
views.argument.taxonomy_index_tid_depth:
type: views_argument
label: 'Taxonomy term ID'
mapping:
depth:
type: integer
label: 'Depth'
break_phrase:
type: boolean
label: 'Allow multiple values'
use_taxonomy_term_path:
type: boolean
label: 'Use taxonomy term path'
views.argument.taxonomy_index_tid_depth_modifier:
type: views_argument
label: 'Taxonomy depth modifier'
views.argument.taxonomy:
type: views_argument
label: 'Taxonomy'
mapping:
break_phrase:
type: boolean
label: 'Allow multiple values'
not:
type: boolean
label: 'Exclude'
views.argument.vocabulary_vid:
type: views_argument
label: 'Vocabulary'
mapping:
break_phrase:
type: boolean
label: 'Allow multiple values'
not:
type: boolean
label: 'Exclude'
views.argument_validator.entity:taxonomy_term:
type: views.argument_validator_entity
label: 'Taxonomy term'
views.argument_validator.taxonomy_term_name:
type: views.argument_validator_entity
label: 'Taxonomy term'
mapping:
vids:
type: sequence
label: 'Vocabularies'
sequence:
type: string
label: 'Vocabulary'
transform:
type: boolean
label: 'Transform dashes in URL to spaces in term name filter values'
views.argument_default.taxonomy_tid:
type: mapping
label: 'Taxonomy term ID from URL'
mapping:
term_page:
type: string
label: 'Load default filter from term page'
node:
type: boolean
label: 'Load default filter from node page, that''s good for related taxonomy blocks'
limit:
type: boolean
label: 'Limit terms by vocabulary'
vids:
type: sequence
label: 'Vocabularies'
sequence:
type: string
label: 'Vocabulary'
anyall:
type: string
label: 'Multiple-value handling'
views.field.term_name:
type: views.field.field
mapping:
convert_spaces:
type: boolean
label: 'Convert spaces in term names to hyphens'
views.field.taxonomy_index_tid:
type: views_field
label: 'Taxonomy language'
mapping:
type:
type: string
label: 'Display type'
separator:
type: string
label: 'Separator'
link_to_taxonomy:
type: boolean
label: 'Link this field to its term page'
limit:
type: boolean
label: 'Limit terms by vocabulary'
vids:
type: sequence
label: 'Vocabularies'
sequence:
type: string
label: 'Vocabulary'
views.filter.taxonomy_index_tid:
type: views.filter.many_to_one
label: 'Taxonomy term ID'
mapping:
vid:
type: string
label: 'Vocabulary'
type:
type: string
label: 'Selection type'
hierarchy:
type: boolean
label: 'Show hierarchy in dropdown'
limit:
type: boolean
label: 'Limit to vocabulary'
error_message:
type: boolean
label: 'Display error message'
value:
type: sequence
label: 'Values'
sequence:
type: integer
label: 'Value'
views.filter.taxonomy_index_tid_depth:
type: views.filter.taxonomy_index_tid
label: 'Taxonomy term ID with depth'
mapping:
depth:
type: integer
label: 'Depth'
views.relationship.node_term_data:
type: views_relationship
label: 'Taxonomy term'
mapping:
vids:
type: sequence
label: 'Vocabularies'
sequence:
type: string
label: 'Vocabulary'

View file

@ -0,0 +1,10 @@
.taxonomy-term-preview {
background-color: #eee;
}
.taxonomy-term-divider-top {
border-bottom: none;
}
.taxonomy-term-divider-bottom {
border-top: 1px dotted #ccc;
}

View file

@ -0,0 +1,38 @@
id: d6_taxonomy_term
label: Taxonomy terms
audit: true
migration_tags:
- Drupal 6
- Content
source:
plugin: d6_taxonomy_term
process:
# If you are using this file to build a custom migration consider removing
# the tid field to allow incremental migrations.
tid: tid
vid:
plugin: migration_lookup
migration: d6_taxonomy_vocabulary
source: vid
name: name
description: description
weight: weight
# Only attempt to stub real (non-zero) parents.
parent_id:
-
plugin: skip_on_empty
method: process
source: parent
-
plugin: migration_lookup
migration: d6_taxonomy_term
parent:
plugin: default_value
default_value: 0
source: '@parent_id'
changed: timestamp
destination:
plugin: entity:taxonomy_term
migration_dependencies:
required:
- d6_taxonomy_vocabulary

View file

@ -0,0 +1,31 @@
id: d6_taxonomy_vocabulary
label: Taxonomy vocabularies
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_taxonomy_vocabulary
process:
vid:
-
plugin: machine_name
source: name
-
plugin: make_unique_entity_field
entity_type: taxonomy_vocabulary
field: vid
length: 32
migrated: true
-
# This plugin checks if the vocabulary being migrated is the one used by
# Forum. If so, we use the machine name that Forum expects. Otherwise, we
# leave it unchanged.
plugin: forum_vocabulary
machine_name: forums
label: name
name: name
description: description
hierarchy: hierarchy
weight: weight
destination:
plugin: entity:taxonomy_vocabulary

View file

@ -0,0 +1,26 @@
id: d6_term_node
label: Term/node relationships
migration_tags:
- Drupal 6
- Content
deriver: Drupal\taxonomy\Plugin\migrate\D6TermNodeDeriver
source:
plugin: d6_term_node
process:
nid:
-
plugin: migration_lookup
migration: d6_node
source: nid
-
plugin: skip_on_empty
method: row
type: type
# The actual field name is dynamic and will be added by the builder.
destination:
plugin: entity:node
migration_dependencies:
required:
- d6_vocabulary_entity_display
- d6_vocabulary_entity_form_display
- d6_node

View file

@ -0,0 +1,26 @@
id: d6_term_node_revision
label: Term/node relationship revisions
audit: true
migration_tags:
- Drupal 6
- Content
deriver: Drupal\taxonomy\Plugin\migrate\D6TermNodeDeriver
source:
plugin: d6_term_node_revision
process:
vid:
-
plugin: migration_lookup
migration: d6_node_revision
source: vid
-
plugin: skip_on_empty
method: row
type: type
# The actual field name is dynamic and will be added by the builder.
destination:
plugin: entity_revision:node
migration_dependencies:
required:
- d6_term_node
- d6_node_revision

View file

@ -0,0 +1,58 @@
id: d6_vocabulary_entity_display
label: Vocabulary display configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_taxonomy_vocabulary_per_type
constants:
entity_type: node
view_mode: default
options:
label: hidden
type: entity_reference_label
weight: 20
field_prefix: field_
process:
entity_type: 'constants/entity_type'
view_mode: 'constants/view_mode'
options: 'constants/options'
bundle:
-
plugin: migration_lookup
migration: d6_node_type
source: type
-
plugin: skip_on_empty
method: row
# This value is only used in the 'field_name' process pipeline below.
raw_field_name:
-
plugin: migration_lookup
migration: d6_taxonomy_vocabulary
source: vid
-
plugin: skip_on_empty
method: row
field_name:
# Prepend field_ to avoid conflicts with base fields, and make sure the
# result is no longer than 32 characters.
-
plugin: concat
source:
- constants/field_prefix
- '@raw_field_name'
-
plugin: substr
length: 32
-
# This plugin checks if the vocabulary being migrated is the one used by
# Forum. If so, we use the machine name that Forum expects. Otherwise, we
# leave it unchanged.
plugin: forum_vocabulary
machine_name: taxonomy_forums
destination:
plugin: component_entity_display
migration_dependencies:
required:
- d6_vocabulary_field_instance

View file

@ -0,0 +1,62 @@
id: d6_vocabulary_entity_form_display
label: Vocabulary form display configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_taxonomy_vocabulary_per_type
constants:
entity_type: node
form_mode: default
options:
weight: 20
field_prefix: field_
process:
entity_type: 'constants/entity_type'
form_mode: 'constants/form_mode'
options/type:
plugin: static_map
source: tags
map:
0: options_select
1: entity_reference_autocomplete_tags
options/weight: 'constants/options/weight'
bundle:
-
plugin: migration_lookup
migration: d6_node_type
source: type
-
plugin: skip_on_empty
method: row
# This value is only used in the 'field_name' process pipeline below.
raw_field_name:
-
plugin: migration_lookup
migration: d6_taxonomy_vocabulary
source: vid
-
plugin: skip_on_empty
method: row
field_name:
# Prepend field_ to avoid conflicts with base fields, and make sure the
# result is no longer than 32 characters.
-
plugin: concat
source:
- constants/field_prefix
- '@raw_field_name'
-
plugin: substr
length: 32
-
# This plugin checks if the vocabulary being migrated is the one used by
# Forum. If so, we use the machine name that Forum expects. Otherwise, we
# leave it unchanged.
plugin: forum_vocabulary
machine_name: taxonomy_forums
destination:
plugin: component_entity_form_display
migration_dependencies:
required:
- d6_vocabulary_field_instance

View file

@ -0,0 +1,51 @@
id: d6_vocabulary_field
label: Vocabulary field configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_taxonomy_vocabulary
constants:
entity_type: node
type: entity_reference
target_entity_type: taxonomy_term
field_prefix: field_
process:
entity_type: 'constants/entity_type'
type: 'constants/type'
# This value is only used in the 'field_name' process pipeline below.
raw_field_name:
-
plugin: migration_lookup
migration: d6_taxonomy_vocabulary
source: vid
-
plugin: skip_on_empty
method: row
field_name:
# Prepend field_ to avoid conflicts with base fields, and make sure the
# result is no longer than 32 characters.
-
plugin: concat
source:
- constants/field_prefix
- '@raw_field_name'
-
plugin: substr
length: 32
-
# This plugin checks if the vocabulary being migrated is the one used by
# Forum. If so, we use the machine name that Forum expects. Otherwise, we
# leave it unchanged.
plugin: forum_vocabulary
machine_name: taxonomy_forums
'settings/target_type': 'constants/target_entity_type'
cardinality: cardinality
destination:
plugin: entity:field_storage_config
dependencies:
module:
- entity_reference
migration_dependencies:
required:
- d6_taxonomy_vocabulary

View file

@ -0,0 +1,74 @@
id: d6_vocabulary_field_instance
label: Vocabulary field instance configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_taxonomy_vocabulary_per_type
constants:
entity_type: node
auto_create: true
selection_handler: 'default:taxonomy_term'
field_prefix: field_
process:
entity_type: 'constants/entity_type'
bundle:
-
plugin: migration_lookup
migration: d6_node_type
source: type
-
plugin: skip_on_empty
method: row
# This value is only used in the 'field_name' process pipeline below.
raw_field_name:
-
plugin: migration_lookup
migration: d6_taxonomy_vocabulary
source: vid
-
plugin: skip_on_empty
method: row
field_name:
# Prepend field_ to avoid conflicts with base fields, and make sure the
# result is no longer than 32 characters.
-
plugin: concat
source:
- constants/field_prefix
- '@raw_field_name'
-
plugin: substr
length: 32
-
# This plugin checks if the vocabulary being migrated is the one used by
# Forum. If so, we use the machine name that Forum expects. Otherwise, we
# leave it unchanged.
plugin: forum_vocabulary
machine_name: taxonomy_forums
label: name
'settings/handler': 'constants/selection_handler'
'settings/handler_settings/target_bundles/0': '@field_name'
'settings/handler_settings/auto_create': 'constants/auto_create'
required: required
# 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.
translatable:
plugin: static_map
source: i18ntaxonomy_vocabulary
default_value: 0
map:
0: false
1: false
2: false
3: true
destination:
plugin: entity:field_config
migration_dependencies:
required:
- d6_node_type
- d6_vocabulary_field

View file

@ -0,0 +1,44 @@
id: d7_taxonomy_term
label: Taxonomy terms
audit: true
migration_tags:
- Drupal 7
- Content
deriver: Drupal\taxonomy\Plugin\migrate\D7TaxonomyTermDeriver
source:
plugin: d7_taxonomy_term
process:
# If you are using this file to build a custom migration consider removing
# the tid field to allow incremental migrations.
tid: tid
vid:
plugin: migration_lookup
migration: d7_taxonomy_vocabulary
source: vid
name: name
'description/value': description
'description/format': format
weight: weight
# Only attempt to stub real (non-zero) parents.
parent_id:
-
plugin: skip_on_empty
method: process
source: parent
-
plugin: migration_lookup
migration: d7_taxonomy_term
parent:
plugin: default_value
default_value: 0
source: '@parent_id'
forum_container: is_container
changed: timestamp
langcode: language
destination:
plugin: entity:taxonomy_term
migration_dependencies:
required:
- d7_taxonomy_vocabulary
optional:
- d7_field_instance

View file

@ -0,0 +1,29 @@
id: d7_taxonomy_vocabulary
label: Taxonomy vocabularies
migration_tags:
- Drupal 7
- Configuration
source:
plugin: d7_taxonomy_vocabulary
process:
vid:
-
plugin: make_unique_entity_field
source: machine_name
entity_type: taxonomy_vocabulary
field: vid
length: 32
migrated: true
-
# This plugin checks if the vocabulary being migrated is the one used by
# Forum. If so, we use the machine name that Forum expects. Otherwise, we
# leave it unchanged.
plugin: forum_vocabulary
machine_name: forums
label: name
name: name
description: description
hierarchy: hierarchy
weight: weight
destination:
plugin: entity:taxonomy_vocabulary

View file

@ -0,0 +1,18 @@
id: taxonomy_settings
label: Taxonomy configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- taxonomy_override_selector
- taxonomy_terms_per_page_admin
source_module: taxonomy
process:
override_selector: taxonomy_override_selector
terms_per_page_admin: taxonomy_terms_per_page_admin
destination:
plugin: config
config_name: taxonomy.settings

View file

@ -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->entityManager()->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()];
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,267 @@
<?php
namespace Drupal\taxonomy\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityPublishedTrait;
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",
* uri_callback = "taxonomy_term_uri",
* translatable = TRUE,
* entity_keys = {
* "id" = "tid",
* "bundle" = "vid",
* "label" = "name",
* "langcode" = "langcode",
* "uuid" = "uuid",
* "published" = "status",
* },
* 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"
* )
*/
class Term extends ContentEntityBase implements TermInterface {
use EntityChangedTrait;
use EntityPublishedTrait;
/**
* {@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);
// Add the published field.
$fields += static::publishedBaseFieldDefinitions($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['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)
->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)
->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);
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();
}
}

View file

@ -0,0 +1,187 @@
<?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",
* "hierarchy",
* "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 type of hierarchy allowed within the vocabulary.
*
* Possible values:
* - VocabularyInterface::HIERARCHY_DISABLED: No parents.
* - VocabularyInterface::HIERARCHY_SINGLE: Single parent.
* - VocabularyInterface::HIERARCHY_MULTIPLE: Multiple parents.
*
* @var int
*/
protected $hierarchy = VocabularyInterface::HIERARCHY_DISABLED;
/**
* The weight of this vocabulary in relation to other vocabularies.
*
* @var int
*/
protected $weight = 0;
/**
* {@inheritdoc}
*/
public function getHierarchy() {
return $this->hierarchy;
}
/**
* {@inheritdoc}
*/
public function setHierarchy($hierarchy) {
$this->hierarchy = $hierarchy;
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.
entity_delete_multiple('taxonomy_term', $storage->getToplevelTids(array_keys($entities)));
}
/**
* {@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 = entity_load_multiple_by_properties('field_storage_config', ['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();
}
}
}
}
}

View file

@ -0,0 +1,512 @@
<?php
namespace Drupal\taxonomy\Form;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
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 {
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* 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;
/**
* Constructs an OverviewTerms object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
*/
public function __construct(ModuleHandlerInterface $module_handler, EntityManagerInterface $entity_manager, RendererInterface $renderer = NULL) {
$this->moduleHandler = $module_handler;
$this->entityManager = $entity_manager;
$this->storageController = $entity_manager->getStorage('taxonomy_term');
$this->termListBuilder = $entity_manager->getListBuilder('taxonomy_term');
$this->renderer = $renderer ?: \Drupal::service('renderer');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
$container->get('entity.manager'),
$container->get('renderer')
);
}
/**
* {@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) {
// @todo Remove global variables when https://www.drupal.org/node/2044435 is
// in.
global $pager_page_array, $pager_total, $pager_total_items;
$form_state->set(['taxonomy', 'vocabulary'], $taxonomy_vocabulary);
$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;
$pager_total_items[0] = $total_entries;
$pager_page_array[0] = $page;
$pager_total[0] = ceil($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]);
}
}
}
$errors = $form_state->getErrors();
$row_position = 0;
// Build the actual form.
$access_control_handler = $this->entityManager->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' => $this->t('Weight'),
],
'#attributes' => [
'id' => 'taxonomy',
],
];
$this->renderer->addCacheableDependency($form['terms'], $create_access);
// Only allow access to changing weights if the user has update access for
// all terms.
$change_weight_access = AccessResult::allowed();
foreach ($current_page as $key => $term) {
$form['terms'][$key] = [
'term' => [],
'operations' => [],
'weight' => [],
];
/** @var $term \Drupal\Core\Entity\EntityInterface */
$term = $this->entityManager->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) ? \Drupal::service('renderer')->render($indentation) : '',
'#type' => 'link',
'#title' => $term->getName(),
'#url' => $term->urlInfo(),
];
if ($taxonomy_vocabulary->getHierarchy() != VocabularyInterface::HIERARCHY_MULTIPLE && 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);
$change_weight_access = $change_weight_access->andIf($update_access);
if ($update_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,
];
}
$form['terms'][$key]['#attributes']['class'] = [];
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'], $change_weight_access);
if ($change_weight_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 (($taxonomy_vocabulary->getHierarchy() !== VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) && $change_weight_access->isAllowed()) {
$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']);
// Update the current hierarchy type as we go.
$hierarchy = VocabularyInterface::HIERARCHY_DISABLED;
$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++;
$hierarchy = $term->parents[0] != 0 ? VocabularyInterface::HIERARCHY_SINGLE : $hierarchy;
$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;
}
$hierarchy = $term->parents[0] != 0 ? VocabularyInterface::HIERARCHY_SINGLE : $hierarchy;
$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;
}
$hierarchy = $term->parents[0] != 0 ? VocabularyInterface::HIERARCHY_SINGLE : $hierarchy;
}
// Save all updated terms.
foreach ($changed_terms as $term) {
$term->save();
}
// Update the vocabulary hierarchy to flat or single hierarchy.
if ($vocabulary->getHierarchy() != $hierarchy) {
$vocabulary->setHierarchy($hierarchy);
$vocabulary->save();
}
$this->messenger()->addStatus($this->t('The configuration options have been saved.'));
}
/**
* 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->urlInfo('reset-form'));
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace Drupal\taxonomy\Form;
use Drupal\Core\Form\FormStateInterface;
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()]);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
/** @var \Drupal\Core\Entity\ContentEntityInterface $term */
$term = $this->getEntity();
if ($term->isDefaultTranslation()) {
$storage = $this->entityManager->getStorage('taxonomy_vocabulary');
$vocabulary = $storage->load($this->entity->bundle());
// @todo Move to storage http://drupal.org/node/1988712
taxonomy_check_vocabulary_hierarchy($vocabulary, ['tid' => $term->id()]);
}
}
}

View file

@ -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()]);
}
}

View file

@ -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.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->urlInfo('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());
}
}

View file

@ -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->entityManager->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->entityManager->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->entityManager->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;
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Drupal\taxonomy\Plugin\Field\FieldFormatter;
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() ? \Drupal::url('entity.taxonomy_term.canonical', ['taxonomy_term' => $entity->id()], ['absolute' => TRUE]) : '',
],
];
}
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';
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,170 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
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\Plugin\MigrateCckFieldPluginManagerInterface;
use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
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;
/**
* Already-instantiated cckfield plugins, keyed by ID.
*
* @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldInterface[]
*/
protected $cckPluginCache;
/**
* The CCK plugin manager.
*
* @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface
*/
protected $cckPluginManager;
/**
* Already-instantiated field plugins, keyed by ID.
*
* @var \Drupal\migrate_drupal\Plugin\MigrateFieldInterface[]
*/
protected $fieldPluginCache;
/**
* The field plugin manager.
*
* @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
*/
protected $fieldPluginManager;
/**
* D7TaxonomyTermDeriver constructor.
*
* @param string $base_plugin_id
* The base plugin ID for the plugin ID.
* @param \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface $cck_manager
* The CCK plugin manager.
* @param \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface $field_manager
* The field plugin manager.
*/
public function __construct($base_plugin_id, MigrateCckFieldPluginManagerInterface $cck_manager, MigrateFieldPluginManagerInterface $field_manager) {
$this->basePluginId = $base_plugin_id;
$this->cckPluginManager = $cck_manager;
$this->fieldPluginManager = $field_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$base_plugin_id,
$container->get('plugin.manager.migrate.cckfield'),
$container->get('plugin.manager.migrate.field')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$fields = [];
try {
$source_plugin = static::getSourcePlugin('d7_field_instance');
$source_plugin->checkRequirements();
// Read all field instance definitions in the source database.
foreach ($source_plugin as $row) {
if ($row->getSourceProperty('entity_type') == 'taxonomy_term') {
$fields[$row->getSourceProperty('bundle')][$row->getSourceProperty('field_name')] = $row->getSource();
}
}
}
catch (RequirementsException $e) {
// If checkRequirements() failed then the field module did not exist and
// we do not have any fields. Therefore, $fields will be empty and below
// we'll create a migration just for the node properties.
}
$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 Migration $migration */
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($values);
if (isset($fields[$bundle])) {
foreach ($fields[$bundle] as $field_name => $info) {
$field_type = $info['type'];
try {
$plugin_id = $this->fieldPluginManager->getPluginIdFromFieldType($field_type, ['core' => 7], $migration);
if (!isset($this->fieldPluginCache[$field_type])) {
$this->fieldPluginCache[$field_type] = $this->fieldPluginManager->createInstance($plugin_id, ['core' => 7], $migration);
}
$this->fieldPluginCache[$field_type]
->defineValueProcessPipeline($migration, $field_name, $info);
}
catch (PluginNotFoundException $ex) {
try {
$plugin_id = $this->cckPluginManager->getPluginIdFromFieldType($field_type, ['core' => 7], $migration);
if (!isset($this->cckPluginCache[$field_type])) {
$this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($plugin_id, ['core' => 7], $migration);
}
$this->cckPluginCache[$field_type]
->processCckFieldValues($migration, $field_name, $info);
}
catch (PluginNotFoundException $ex) {
$migration->setProcessOfProperty($field_name, $field_name);
}
}
}
}
$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;
}
}

View file

@ -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.x, to be removed before Drupal 9.0.x. 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);
}
}

View file

@ -0,0 +1,44 @@
<?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',
];
}
/**
* {@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);
}
}

View file

@ -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;
}
}

View file

@ -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, intended to be removed in 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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,100 @@
<?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->isNotNull('i18n.lid');
$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();
$row->setSourceProperty($other_property . '_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;
}
}

View file

@ -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;
}
}

View file

@ -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';
}

View file

@ -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;
}
}

View file

@ -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', NULL);
$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;
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Drupal\taxonomy\Plugin\migrate\source\d6;
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', ['vid', 'name', 'description'])
->fields('i18n', ['lid', 'type', 'property', 'objectid'])
->fields('lt', ['lid', 'translation'])
->condition('i18n.type', 'vocabulary');
$query->addField('lt', 'language', '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->leftJoin('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 getIds() {
$ids['vid']['type'] = 'integer';
return $ids;
}
}

View file

@ -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;
/**
* 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 (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);
$source_language = $this->getEntityTranslationSourceLanguage('taxonomy_term', $tid);
$language = $entity_translatable && $source_language ? $source_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;
}
}

View file

@ -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',
],
];
}
}

View file

@ -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;
}
}

View file

@ -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::entityManager()->getTranslationFromContext($term)->label();
}
return $titles;
}
}

View file

@ -0,0 +1,144 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument;
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.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 = db_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');
}
}

View file

@ -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;
}
}
}

View file

@ -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.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');
}
}

View file

@ -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.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');
}
}

View file

@ -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.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;
}
}

View file

@ -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;
}
}
}
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace Drupal\taxonomy\Plugin\views\argument_validator;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\EntityManagerInterface;
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, EntityManagerInterface $entity_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_manager);
// Not handling exploding term names.
$this->multipleCapable = FALSE;
$this->termStorage = $entity_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);
}
$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;
}
}

View file

@ -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.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::entityManager()->getStorage('taxonomy_term')->getNodeTerms($nids, $vocabs);
foreach ($result as $node_nid => $data) {
foreach ($data as $tid => $term) {
$this->items[$node_nid][$tid]['name'] = \Drupal::entityManager()->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] : '';
}
}
}

View file

@ -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);
}
}

View file

@ -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.manager')->getStorage('taxonomy_vocabulary'),
$container->get('entity.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::entityManager()->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::entityManager()->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::entityManager()->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;
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace Drupal\taxonomy\Plugin\views\filter;
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 = db_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');
}
}

View file

@ -0,0 +1,164 @@
<?php
namespace Drupal\taxonomy\Plugin\views\relationship;
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.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 = db_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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityManagerInterface;
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;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a TaxonomyPermissions instance.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(EntityManagerInterface $entity_manager) {
$this->entityManager = $entity_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('entity.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)],
];
}
}

View file

@ -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');
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Entity\EntityManagerInterface;
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;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The taxonomy storage.
*
* @var \Drupal\Taxonomy\TermStorageInterface
*/
protected $termStorage;
/**
* Constructs the TermBreadcrumbBuilder.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entityManager
* The entity manager.
*/
public function __construct(EntityManagerInterface $entityManager) {
$this->entityManager = $entityManager;
$this->termStorage = $entityManager->getStorage('taxonomy_term');
}
/**
* {@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->entityManager->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;
}
}

View file

@ -0,0 +1,169 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\ContentEntityForm;
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->entityManager->getStorage('taxonomy_vocabulary');
$taxonomy_storage = $this->entityManager->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' => $vocabulary->getHierarchy() == 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_keys($form_state->getValue('parent'));
return $term;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$term = $this->entity;
$result = $term->save();
$edit_link = $term->link($this->t('Edit'), 'edit-form');
$view_link = $term->link($term->getName());
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'));
$previous_parent_count = count($form_state->get(['taxonomy', 'parent']));
// Root doesn't count if it's the only parent.
if ($current_parent_count == 1 && $form_state->hasValue(['parent', 0])) {
$current_parent_count = 0;
$form_state->setValue('parent', []);
}
// If the number of parents has been reduced to one or none, do a check on the
// parents of every term in the vocabulary value.
$vocabulary = $form_state->get(['taxonomy', 'vocabulary']);
if ($current_parent_count < $previous_parent_count && $current_parent_count < 2) {
taxonomy_check_vocabulary_hierarchy($vocabulary, $form_state->getValues());
}
// If we've increased the number of parents and this is a single or flat
// hierarchy, update the vocabulary immediately.
elseif ($current_parent_count > $previous_parent_count && $vocabulary->getHierarchy() != VocabularyInterface::HIERARCHY_MULTIPLE) {
$vocabulary->setHierarchy($current_parent_count == 1 ? VocabularyInterface::HIERARCHY_SINGLE : VocabularyInterface::HIERARCHY_MULTIPLE);
$vocabulary->save();
}
$form_state->setValue('tid', $term->id());
$form_state->set('tid', $term->id());
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
/**
* Provides an interface defining a taxonomy term entity.
*/
interface TermInterface extends ContentEntityInterface, EntityChangedInterface, EntityPublishedInterface {
/**
* Gets the term's description.
*
* @return string
* The term description.
*/
public function getDescription();
/**
* Sets the term's description.
*
* @param string $description
* The term's description.
*
* @return $this
*/
public function setDescription($description);
/**
* Gets the text format name for the term's description.
*
* @return string
* The text format name.
*/
public function getFormat();
/**
* Sets the text format name for the term's description.
*
* @param string $format
* The term's description text format.
*
* @return $this
*/
public function setFormat($format);
/**
* Gets the name of the term.
*
* @return string
* The name of the term.
*/
public function getName();
/**
* Sets the name of the term.
*
* @param string $name
* The term's name.
*
* @return $this
*/
public function setName($name);
/**
* Gets the weight of this term.
*
* @return int
* The weight of the term.
*/
public function getWeight();
/**
* Gets the weight of this term.
*
* @param int $weight
* The term's weight.
*
* @return $this
*/
public function setWeight($weight);
/**
* Get the taxonomy vocabulary id this term belongs to.
*
* @return string
* The id of the vocabulary.
*
* @deprecated Scheduled for removal before Drupal 9.0.0. Use
* TermInterface::bundle() instead.
*/
public function getVocabularyId();
}

View file

@ -0,0 +1,383 @@
<?php
namespace Drupal\taxonomy;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
/**
* Defines a Controller class for taxonomy terms.
*/
class TermStorage extends SqlContentEntityStorage implements TermStorageInterface {
/**
* Array of 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;
/**
* {@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 = [];
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 = db_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 __sleep() {
$vars = parent::__sleep();
// Do not serialize static cache.
unset($vars['ancestors'], $vars['treeChildren'], $vars['treeParents'], $vars['treeTerms'], $vars['trees']);
return $vars;
}
/**
* {@inheritdoc}
*/
public function __wakeup() {
parent::__wakeup();
// Initialize static caches.
$this->ancestors = [];
$this->treeChildren = [];
$this->treeParents = [];
$this->treeTerms = [];
$this->trees = [];
}
}

View file

@ -0,0 +1,129 @@
<?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);
}

View file

@ -0,0 +1,111 @@
<?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;
}
}

View file

@ -0,0 +1,39 @@
<?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['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->urlInfo('edit-form'));
}
}
}

View file

@ -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.x and will be removed before 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 {}

View file

@ -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',
'base field' => 'nid',
'label' => $this->t('node'),
'skip base' => 'node',
],
];
// @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;
}
}

View file

@ -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\Functional\TaxonomyTestTrait;
/**
* Provides common helper methods for Taxonomy module tests.
*
* @deprecated Scheduled for removal in 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']);
}
}
}

View file

@ -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\Functional\TaxonomyTestTrait', 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 will be removed before Drupal 9.0.0.
* Use \Drupal\Tests\taxonomy\Functional\TaxonomyTestTrait
*/
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;
}
}

View file

@ -0,0 +1,106 @@
<?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 will be removed before 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_static_reset();
\Drupal::entityManager()->clearCachedDefinitions();
\Drupal::service('router.builder')->rebuild();
\Drupal::service('entity.definition_update_manager')->applyUpdates();
}
/**
* 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();
entity_get_form_display('node', 'article', 'default')
->setComponent($this->termFieldName, [
'type' => 'entity_reference_autocomplete_tags',
])
->save();
entity_get_display('node', 'article', 'default')
->setComponent($this->termFieldName, [
'type' => 'entity_reference_label',
])
->save();
}
}

View file

@ -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);
}
}
}

View file

@ -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.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->link($this->t('Edit'));
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->urlInfo('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->urlInfo('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);
}
}

View file

@ -0,0 +1,57 @@
<?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.
*/
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
*/
public function setHierarchy($hierarchy);
/**
* Returns the vocabulary description.
*
* @return string
* The vocabulary description.
*/
public function getDescription();
}

View file

@ -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.'));
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -0,0 +1,62 @@
/**
* @file
* Taxonomy behaviors.
*/
(function($, Drupal) {
/**
* Move a block in the blocks table from one region to another.
*
* This behavior is dependent on the tableDrag behavior, since it uses the
* objects initialized in that behavior to update the row.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the drag behavior to a applicable table element.
*/
Drupal.behaviors.termDrag = {
attach(context, settings) {
const backStep = settings.taxonomy.backStep;
const forwardStep = settings.taxonomy.forwardStep;
// Get the blocks tableDrag object.
const tableDrag = Drupal.tableDrag.taxonomy;
const $table = $('#taxonomy');
const rows = $table.find('tr').length;
// When a row is swapped, keep previous and next page classes set.
tableDrag.row.prototype.onSwap = function(swappedRow) {
$table
.find('tr.taxonomy-term-preview')
.removeClass('taxonomy-term-preview');
$table
.find('tr.taxonomy-term-divider-top')
.removeClass('taxonomy-term-divider-top');
$table
.find('tr.taxonomy-term-divider-bottom')
.removeClass('taxonomy-term-divider-bottom');
const tableBody = $table[0].tBodies[0];
if (backStep) {
for (let n = 0; n < backStep; n++) {
$(tableBody.rows[n]).addClass('taxonomy-term-preview');
}
$(tableBody.rows[backStep - 1]).addClass('taxonomy-term-divider-top');
$(tableBody.rows[backStep]).addClass('taxonomy-term-divider-bottom');
}
if (forwardStep) {
for (let k = rows - forwardStep - 1; k < rows - 1; k++) {
$(tableBody.rows[k]).addClass('taxonomy-term-preview');
}
$(tableBody.rows[rows - forwardStep - 2]).addClass(
'taxonomy-term-divider-top',
);
$(tableBody.rows[rows - forwardStep - 1]).addClass(
'taxonomy-term-divider-bottom',
);
}
};
},
};
})(jQuery, Drupal);

View file

@ -0,0 +1,10 @@
name: Taxonomy
type: module
description: 'Enables the categorization of content.'
package: Core
version: VERSION
core: 8.x
dependencies:
- drupal:node
- drupal:text
configure: entity.taxonomy_vocabulary.collection

View file

@ -0,0 +1,188 @@
<?php
/**
* @file
* Install, update and uninstall functions for the taxonomy module.
*/
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Site\Settings;
/**
* Convert the custom taxonomy term hierarchy storage to a default storage.
*/
function taxonomy_update_8501() {
$definition_update_manager = \Drupal::entityDefinitionUpdateManager();
/** @var \Drupal\Core\Field\BaseFieldDefinition $field_storage_definition */
$field_storage_definition = $definition_update_manager->getFieldStorageDefinition('parent', 'taxonomy_term');
$field_storage_definition->setCustomStorage(FALSE);
$definition_update_manager->updateFieldStorageDefinition($field_storage_definition);
}
/**
* Copy hierarchy from {taxonomy_term_hierarchy} to {taxonomy_term__parent}.
*/
function taxonomy_update_8502(&$sandbox) {
$database = \Drupal::database();
if (!isset($sandbox['current'])) {
// Set batch ops sandbox.
$sandbox['current'] = 0;
$sandbox['tid'] = -1;
$sandbox['delta'] = 0;
$sandbox['limit'] = Settings::get('entity_update_batch_size', 50);
// Count records using a join, as there might be orphans in the hierarchy
// table. See https://www.drupal.org/project/drupal/issues/2997982.
$select = $database->select('taxonomy_term_hierarchy', 'h');
$select->join('taxonomy_term_data', 'd', 'h.tid = d.tid');
$sandbox['max'] = $select
->countQuery()
->execute()
->fetchField();
}
// Save the hierarchy.
$select = $database->select('taxonomy_term_hierarchy', 'h');
$select->join('taxonomy_term_data', 'd', 'h.tid = d.tid');
$hierarchy = $select
->fields('h', ['tid', 'parent'])
->fields('d', ['vid', 'langcode'])
->range($sandbox['current'], $sandbox['limit'])
->orderBy('tid', 'ASC')
->orderBy('parent', 'ASC')
->execute()
->fetchAll();
// Restore data.
$insert = $database->insert('taxonomy_term__parent')
->fields(['bundle', 'entity_id', 'revision_id', 'langcode', 'delta', 'parent_target_id']);
foreach ($hierarchy as $row) {
if ($row->tid !== $sandbox['tid']) {
$sandbox['delta'] = 0;
$sandbox['tid'] = $row->tid;
}
$insert->values([
'bundle' => $row->vid,
'entity_id' => $row->tid,
'revision_id' => $row->tid,
'langcode' => $row->langcode,
'delta' => $sandbox['delta'],
'parent_target_id' => $row->parent,
]);
$sandbox['delta']++;
$sandbox['current']++;
}
$insert->execute();
$sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['current'] / $sandbox['max']);
if ($sandbox['#finished'] >= 1) {
// Update the entity type because the 'taxonomy_term_hierarchy' table is no
// longer part of its shared tables schema.
$definition_update_manager = \Drupal::entityDefinitionUpdateManager();
$definition_update_manager->updateEntityType($definition_update_manager->getEntityType('taxonomy_term'));
// \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::onEntityTypeUpdate()
// only deletes *known* entity tables (i.e. the base, data and revision
// tables), so we have to drop it manually.
$database->schema()->dropTable('taxonomy_term_hierarchy');
return t('Taxonomy term hierarchy has been converted to default entity reference storage.');
}
}
/**
* Update views to use {taxonomy_term__parent} in relationships.
*/
function taxonomy_update_8503() {
$config_factory = \Drupal::configFactory();
foreach ($config_factory->listAll('views.view.') as $id) {
$view = $config_factory->getEditable($id);
foreach (array_keys($view->get('display')) as $display_id) {
$changed = FALSE;
foreach (['relationships', 'filters', 'arguments'] as $handler_type) {
$base_path = "display.$display_id.display_options.$handler_type";
$handlers = $view->get($base_path);
if (!$handlers) {
continue;
}
foreach ($handlers as $handler_key => $handler_config) {
$table_path = "$base_path.$handler_key.table";
$field_path = "$base_path.$handler_key.field";
$table = $view->get($table_path);
$field = $view->get($field_path);
if (($table && ($table === 'taxonomy_term_hierarchy')) && ($field && ($field === 'parent'))) {
$view->set($table_path, 'taxonomy_term__parent');
$view->set($field_path, 'parent_target_id');
$changed = TRUE;
}
}
}
if ($changed) {
$view->save(TRUE);
}
}
}
}
/**
* Add the publishing status fields to taxonomy terms.
*/
function taxonomy_update_8601() {
$definition_update_manager = \Drupal::entityDefinitionUpdateManager();
$entity_type = $definition_update_manager->getEntityType('taxonomy_term');
// Bail out early if a field named 'status' is already installed.
if ($definition_update_manager->getFieldStorageDefinition('status', 'taxonomy_term')) {
$message = \Drupal::state()->get('taxonomy_update_8601_skip_message', t('The publishing status field has <strong>not</strong> been added to taxonomy terms. See <a href=":link">this page</a> for more information on how to install it.', [
':link' => 'https://www.drupal.org/node/2985366',
]));
return $message;
}
// Add the 'published' entity key to the taxonomy_term entity type.
$entity_keys = $entity_type->getKeys();
$entity_keys['published'] = 'status';
$entity_type->set('entity_keys', $entity_keys);
$definition_update_manager->updateEntityType($entity_type);
// Add the status field.
$status = BaseFieldDefinition::create('boolean')
->setLabel(t('Publishing status'))
->setDescription(t('A boolean indicating the published state.'))
->setRevisionable(TRUE)
->setTranslatable(TRUE)
->setDefaultValue(TRUE);
$has_content_translation_status_field = $definition_update_manager->getFieldStorageDefinition('content_translation_status', 'taxonomy_term');
if ($has_content_translation_status_field) {
$status->setInitialValueFromField('content_translation_status', TRUE);
}
else {
$status->setInitialValue(TRUE);
}
$definition_update_manager->installFieldStorageDefinition('status', 'taxonomy_term', 'taxonomy_term', $status);
// Uninstall the 'content_translation_status' field if needed.
if ($has_content_translation_status_field) {
$content_translation_status = $definition_update_manager->getFieldStorageDefinition('content_translation_status', 'taxonomy_term');
$definition_update_manager->uninstallFieldStorageDefinition($content_translation_status);
}
return t('The publishing status field has been added to taxonomy terms.');
}

View file

@ -0,0 +1,42 @@
/**
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(function ($, Drupal) {
Drupal.behaviors.termDrag = {
attach: function attach(context, settings) {
var backStep = settings.taxonomy.backStep;
var forwardStep = settings.taxonomy.forwardStep;
var tableDrag = Drupal.tableDrag.taxonomy;
var $table = $('#taxonomy');
var rows = $table.find('tr').length;
tableDrag.row.prototype.onSwap = function (swappedRow) {
$table.find('tr.taxonomy-term-preview').removeClass('taxonomy-term-preview');
$table.find('tr.taxonomy-term-divider-top').removeClass('taxonomy-term-divider-top');
$table.find('tr.taxonomy-term-divider-bottom').removeClass('taxonomy-term-divider-bottom');
var tableBody = $table[0].tBodies[0];
if (backStep) {
for (var n = 0; n < backStep; n++) {
$(tableBody.rows[n]).addClass('taxonomy-term-preview');
}
$(tableBody.rows[backStep - 1]).addClass('taxonomy-term-divider-top');
$(tableBody.rows[backStep]).addClass('taxonomy-term-divider-bottom');
}
if (forwardStep) {
for (var k = rows - forwardStep - 1; k < rows - 1; k++) {
$(tableBody.rows[k]).addClass('taxonomy-term-preview');
}
$(tableBody.rows[rows - forwardStep - 2]).addClass('taxonomy-term-divider-top');
$(tableBody.rows[rows - forwardStep - 1]).addClass('taxonomy-term-divider-bottom');
}
};
}
};
})(jQuery, Drupal);

View file

@ -0,0 +1,12 @@
drupal.taxonomy:
version: VERSION
js:
taxonomy.js: {}
css:
component:
css/taxonomy.theme.css: {}
dependencies:
- core/jquery
- core/drupal
- core/drupalSettings
- core/drupal.tabledrag

View file

@ -0,0 +1,11 @@
entity.taxonomy_vocabulary.add_form:
route_name: entity.taxonomy_vocabulary.add_form
title: 'Add vocabulary'
appears_on:
- entity.taxonomy_vocabulary.collection
entity.taxonomy_term.add_form:
route_name: entity.taxonomy_term.add_form
title: 'Add term'
appears_on:
- entity.taxonomy_vocabulary.overview_form

View file

@ -0,0 +1,17 @@
entity.taxonomy_term.edit_form:
title: Edit
group: taxonomy_term
route_name: entity.taxonomy_term.edit_form
weight: 10
entity.taxonomy_term.delete_form:
title: Delete
group: taxonomy_term
route_name: entity.taxonomy_term.delete_form
weight: 20
entity.taxonomy_vocabulary.delete_form:
title: Delete
group: taxonomy_vocabulary
route_name: entity.taxonomy_vocabulary.delete_form
weight: 20

View file

@ -0,0 +1,5 @@
entity.taxonomy_vocabulary.collection:
title: Taxonomy
parent: system.admin_structure
description: 'Manage tagging, categorization, and classification of your content.'
route_name: entity.taxonomy_vocabulary.collection

View file

@ -0,0 +1,19 @@
entity.taxonomy_term.canonical:
title: 'View'
route_name: entity.taxonomy_term.canonical
base_route: entity.taxonomy_term.canonical
entity.taxonomy_term.edit_form:
title: 'Edit'
route_name: entity.taxonomy_term.edit_form
base_route: entity.taxonomy_term.canonical
entity.taxonomy_vocabulary.overview_form:
title: 'List'
route_name: entity.taxonomy_vocabulary.overview_form
base_route: entity.taxonomy_vocabulary.overview_form
entity.taxonomy_vocabulary.edit_form:
title: 'Edit'
route_name: entity.taxonomy_vocabulary.edit_form
base_route: entity.taxonomy_vocabulary.overview_form

View file

@ -0,0 +1,606 @@
<?php
/**
* @file
* Enables the organization of content into categories.
*/
use Drupal\Component\Utility\Tags;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\taxonomy\TermInterface;
use Drupal\taxonomy\VocabularyInterface;
/**
* Denotes that no term in the vocabulary has a parent.
*
* @deprecated in Drupal 8.2.x and will be removed before 9.0.0. Use
* \Drupal\taxonomy\VocabularyInterface::HIERARCHY_DISABLED instead.
*
* @see https://www.drupal.org/node/2807795
*/
const TAXONOMY_HIERARCHY_DISABLED = 0;
/**
* Denotes that one or more terms in the vocabulary has a single parent.
*
* @deprecated in Drupal 8.2.x and will be removed before 9.0.0. Use
* \Drupal\taxonomy\VocabularyInterface::HIERARCHY_SINGLE instead.
*
* @see https://www.drupal.org/node/2807795
*/
const TAXONOMY_HIERARCHY_SINGLE = 1;
/**
* Denotes that one or more terms in the vocabulary have multiple parents.
*
* @deprecated in Drupal 8.2.x and will be removed before 9.0.0. Use
* \Drupal\taxonomy\VocabularyInterface::HIERARCHY_MULTIPLE instead.
*
* @see https://www.drupal.org/node/2807795
*/
const TAXONOMY_HIERARCHY_MULTIPLE = 2;
/**
* Implements hook_help().
*/
function taxonomy_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.taxonomy':
$field_ui_url = \Drupal::moduleHandler()->moduleExists('field_ui') ? \Drupal::url('help.page', ['name' => 'field_ui']) : '#';
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Taxonomy module allows users who have permission to create and edit content to categorize (tag) content of that type. Users who have the <em>Administer vocabularies and terms</em> <a href=":permissions" title="Taxonomy module permissions">permission</a> can add <em>vocabularies</em> that contain a set of related <em>terms</em>. The terms in a vocabulary can either be pre-set by an administrator or built gradually as content is added and edited. Terms may be organized hierarchically if desired.', [':permissions' => \Drupal::url('user.admin_permissions', [], ['fragment' => 'module-taxonomy'])]) . '</p>';
$output .= '<p>' . t('For more information, see the <a href=":taxonomy">online documentation for the Taxonomy module</a>.', [':taxonomy' => 'https://www.drupal.org/documentation/modules/taxonomy/']) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Managing vocabularies') . '</dt>';
$output .= '<dd>' . t('Users who have the <em>Administer vocabularies and terms</em> permission can add and edit vocabularies from the <a href=":taxonomy_admin">Taxonomy administration page</a>. Vocabularies can be deleted from their <em>Edit vocabulary</em> page. Users with the <em>Taxonomy term: Administer fields</em> permission may add additional fields for terms in that vocabulary using the <a href=":field_ui">Field UI module</a>.', [':taxonomy_admin' => \Drupal::url('entity.taxonomy_vocabulary.collection'), ':field_ui' => $field_ui_url]) . '</dd>';
$output .= '<dt>' . t('Managing terms') . '</dt>';
$output .= '<dd>' . t('Users who have the <em>Administer vocabularies and terms</em> permission or the <em>Edit terms</em> permission for a particular vocabulary can add, edit, and organize the terms in a vocabulary from a vocabulary\'s term listing page, which can be accessed by going to the <a href=":taxonomy_admin">Taxonomy administration page</a> and clicking <em>List terms</em> in the <em>Operations</em> column. Users must have the <em>Administer vocabularies and terms</em> permission or the <em>Delete terms</em> permission for a particular vocabulary to delete terms.', [':taxonomy_admin' => \Drupal::url('entity.taxonomy_vocabulary.collection')]) . ' </dd>';
$output .= '<dt>' . t('Classifying entity content') . '</dt>';
$output .= '<dd>' . t('A user with the <em>Administer fields</em> permission for a certain entity type may add <em>Taxonomy term</em> reference fields to the entity type, which will allow entities to be classified using taxonomy terms. See the <a href=":entity_reference">Entity Reference help</a> for more information about reference fields. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them.', [':field_ui' => $field_ui_url, ':field' => \Drupal::url('help.page', ['name' => 'field']), ':entity_reference' => \Drupal::url('help.page', ['name' => 'entity_reference'])]) . '</dd>';
$output .= '<dt>' . t('Adding new terms during content creation') . '</dt>';
$output .= '<dd>' . t("Allowing users to add new terms gradually builds a vocabulary as content is added and edited. Users can add new terms if either of the two <em>Autocomplete</em> widgets is chosen for the Taxonomy term reference field in the <em>Manage form display</em> page for the field. You will also need to enable the <em>Create referenced entities if they don't already exist</em> option, and restrict the field to one vocabulary.") . '</dd>';
$output .= '<dt>' . t('Configuring displays and form displays') . '</dt>';
$output .= '<dd>' . t('See the <a href=":entity_reference">Entity Reference help</a> page for the field widgets and formatters that can be configured for any reference field on the <em>Manage display</em> and <em>Manage form display</em> pages. Taxonomy additionally provides an <em>RSS category</em> formatter that displays nothing when the entity item is displayed as HTML, but displays an RSS category instead of a list when the entity item is displayed in an RSS feed.', [':entity_reference' => \Drupal::url('help.page', ['name' => 'entity_reference'])]) . '</li>';
$output .= '</ul>';
$output .= '</dd>';
$output .= '</dl>';
return $output;
case 'entity.taxonomy_vocabulary.collection':
$output = '<p>' . t('Taxonomy is for categorizing content. Terms are grouped into vocabularies. For example, a vocabulary called "Fruit" would contain the terms "Apple" and "Banana".') . '</p>';
return $output;
case 'entity.taxonomy_vocabulary.overview_form':
$vocabulary = $route_match->getParameter('taxonomy_vocabulary');
if (\Drupal::currentUser()->hasPermission('administer taxonomy') || \Drupal::currentUser()->hasPermission('edit terms in ' . $vocabulary->id())) {
switch ($vocabulary->getHierarchy()) {
case VocabularyInterface::HIERARCHY_DISABLED:
return '<p>' . t('You can reorganize the terms in %capital_name using their drag-and-drop handles, and group terms under a parent term by sliding them under and to the right of the parent.', ['%capital_name' => Unicode::ucfirst($vocabulary->label()), '%name' => $vocabulary->label()]) . '</p>';
case VocabularyInterface::HIERARCHY_SINGLE:
return '<p>' . t('%capital_name contains terms grouped under parent terms. You can reorganize the terms in %capital_name using their drag-and-drop handles.', ['%capital_name' => Unicode::ucfirst($vocabulary->label()), '%name' => $vocabulary->label()]) . '</p>';
case VocabularyInterface::HIERARCHY_MULTIPLE:
return '<p>' . 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.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
}
}
else {
switch ($vocabulary->getHierarchy()) {
case VocabularyInterface::HIERARCHY_DISABLED:
return '<p>' . t('%capital_name contains the following terms.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
case VocabularyInterface::HIERARCHY_SINGLE:
return '<p>' . t('%capital_name contains terms grouped under parent terms', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
case VocabularyInterface::HIERARCHY_MULTIPLE:
return '<p>' . t('%capital_name contains terms with multiple parents.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
}
}
}
}
/**
* Entity URI callback.
*/
function taxonomy_term_uri($term) {
return new Url('entity.taxonomy_term.canonical', [
'taxonomy_term' => $term->id(),
]);
}
/**
* Implements hook_page_attachments_alter().
*/
function taxonomy_page_attachments_alter(array &$page) {
$route_match = \Drupal::routeMatch();
if ($route_match->getRouteName() == 'entity.taxonomy_term.canonical' && ($term = $route_match->getParameter('taxonomy_term')) && $term instanceof TermInterface) {
foreach ($term->uriRelationships() as $rel) {
// Set the URI relationships, like canonical.
$page['#attached']['html_head_link'][] = [
[
'rel' => $rel,
'href' => $term->url($rel),
],
TRUE,
];
// Set the term path as the canonical URL to prevent duplicate content.
if ($rel == 'canonical') {
// Set the non-aliased canonical path as a default shortlink.
$page['#attached']['html_head_link'][] = [
[
'rel' => 'shortlink',
'href' => $term->url($rel, ['alias' => TRUE]),
],
TRUE,
];
}
}
}
}
/**
* Implements hook_theme().
*/
function taxonomy_theme() {
return [
'taxonomy_term' => [
'render element' => 'elements',
],
];
}
/**
* Checks and updates the hierarchy flag of a vocabulary.
*
* Checks the current parents of all terms in a vocabulary and updates the
* vocabulary's hierarchy setting to the lowest possible level. If no term
* has parent terms then the vocabulary will be given a hierarchy of
* VocabularyInterface::HIERARCHY_DISABLED. If any term has a single parent then
* the vocabulary will be given a hierarchy of
* VocabularyInterface::HIERARCHY_SINGLE. If any term has multiple parents then
* the vocabulary will be given a hierarchy of
* VocabularyInterface::HIERARCHY_MULTIPLE.
*
* @param \Drupal\taxonomy\VocabularyInterface $vocabulary
* A taxonomy vocabulary entity.
* @param $changed_term
* An array of the term structure that was updated.
*
* @return
* An integer that represents the level of the vocabulary's hierarchy.
*/
function taxonomy_check_vocabulary_hierarchy(VocabularyInterface $vocabulary, $changed_term) {
$tree = \Drupal::entityManager()->getStorage('taxonomy_term')->loadTree($vocabulary->id());
$hierarchy = VocabularyInterface::HIERARCHY_DISABLED;
foreach ($tree as $term) {
// Update the changed term with the new parent value before comparison.
if ($term->tid == $changed_term['tid']) {
$term = (object) $changed_term;
$term->parents = $term->parent;
}
// Check this term's parent count.
if (count($term->parents) > 1) {
$hierarchy = VocabularyInterface::HIERARCHY_MULTIPLE;
break;
}
elseif (count($term->parents) == 1 && !isset($term->parents[0])) {
$hierarchy = VocabularyInterface::HIERARCHY_SINGLE;
}
}
if ($hierarchy != $vocabulary->getHierarchy()) {
$vocabulary->setHierarchy($hierarchy);
$vocabulary->save();
}
return $hierarchy;
}
/**
* Generates an array which displays a term detail page.
*
* @param \Drupal\taxonomy\Entity\Term $term
* A taxonomy term object.
* @param string $view_mode
* View mode; e.g., 'full', 'teaser', etc.
* @param string $langcode
* (optional) A language code to use for rendering. Defaults to the global
* content language of the current request.
*
* @return array
* A $page element suitable for use by
* \Drupal\Core\Render\RendererInterface::render().
*/
function taxonomy_term_view(Term $term, $view_mode = 'full', $langcode = NULL) {
return entity_view($term, $view_mode, $langcode);
}
/**
* Constructs a drupal_render() style array from an array of loaded terms.
*
* @param array $terms
* An array of taxonomy terms as returned by Term::loadMultiple().
* @param string $view_mode
* View mode; e.g., 'full', 'teaser', etc.
* @param string $langcode
* (optional) A language code to use for rendering. Defaults to the global
* content language of the current request.
*
* @return array
* An array in the format expected by
* \Drupal\Core\Render\RendererInterface::render().
*/
function taxonomy_term_view_multiple(array $terms, $view_mode = 'full', $langcode = NULL) {
return entity_view_multiple($terms, $view_mode, $langcode);
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function taxonomy_theme_suggestions_taxonomy_term(array $variables) {
$suggestions = [];
/** @var \Drupal\taxonomy\TermInterface $term */
$term = $variables['elements']['#taxonomy_term'];
$suggestions[] = 'taxonomy_term__' . $term->bundle();
$suggestions[] = 'taxonomy_term__' . $term->id();
return $suggestions;
}
/**
* Prepares variables for taxonomy term templates.
*
* Default template: taxonomy-term.html.twig.
*
* @param array $variables
* An associative array containing:
* - elements: An associative array containing the taxonomy term and any
* fields attached to the term. Properties used:
* - #taxonomy_term: A \Drupal\taxonomy\TermInterface object.
* - #view_mode: The current view mode for this taxonomy term, e.g.
* 'full' or 'teaser'.
* - attributes: HTML attributes for the containing element.
*/
function template_preprocess_taxonomy_term(&$variables) {
$variables['view_mode'] = $variables['elements']['#view_mode'];
$variables['term'] = $variables['elements']['#taxonomy_term'];
/** @var \Drupal\taxonomy\TermInterface $term */
$term = $variables['term'];
$variables['url'] = $term->url();
// We use name here because that is what appears in the UI.
$variables['name'] = $variables['elements']['name'];
unset($variables['elements']['name']);
$variables['page'] = $variables['view_mode'] == 'full' && taxonomy_term_is_page($term);
// Helpful $content variable for templates.
$variables['content'] = [];
foreach (Element::children($variables['elements']) as $key) {
$variables['content'][$key] = $variables['elements'][$key];
}
}
/**
* Returns whether the current page is the page of the passed-in term.
*
* @param \Drupal\taxonomy\Entity\Term $term
* A taxonomy term entity.
*/
function taxonomy_term_is_page(Term $term) {
if (\Drupal::routeMatch()->getRouteName() == 'entity.taxonomy_term.canonical' && $page_term_id = \Drupal::routeMatch()->getRawParameter('taxonomy_term')) {
return $page_term_id == $term->id();
}
return FALSE;
}
/**
* Clear all static cache variables for terms.
*/
function taxonomy_terms_static_reset() {
\Drupal::entityManager()->getStorage('taxonomy_term')->resetCache();
}
/**
* Clear all static cache variables for vocabularies.
*
* @param $ids
* An array of ids to reset in the entity cache.
*/
function taxonomy_vocabulary_static_reset(array $ids = NULL) {
\Drupal::entityManager()->getStorage('taxonomy_vocabulary')->resetCache($ids);
}
/**
* Get names for all taxonomy vocabularies.
*
* @return array
* A list of existing vocabulary IDs.
*/
function taxonomy_vocabulary_get_names() {
$names = &drupal_static(__FUNCTION__);
if (!isset($names)) {
$names = [];
$config_names = \Drupal::configFactory()->listAll('taxonomy.vocabulary.');
foreach ($config_names as $config_name) {
$id = substr($config_name, strlen('taxonomy.vocabulary.'));
$names[$id] = $id;
}
}
return $names;
}
/**
* Try to map a string to an existing term, as for glossary use.
*
* Provides a case-insensitive and trimmed mapping, to maximize the
* likelihood of a successful match.
*
* @param $name
* Name of the term to search for.
* @param $vocabulary
* (optional) Vocabulary machine name to limit the search. Defaults to NULL.
*
* @return
* An array of matching term objects.
*/
function taxonomy_term_load_multiple_by_name($name, $vocabulary = NULL) {
$values = ['name' => trim($name)];
if (isset($vocabulary)) {
$vocabularies = taxonomy_vocabulary_get_names();
if (isset($vocabularies[$vocabulary])) {
$values['vid'] = $vocabulary;
}
else {
// Return an empty array when filtering by a non-existing vocabulary.
return [];
}
}
return entity_load_multiple_by_properties('taxonomy_term', $values);
}
/**
* Load multiple taxonomy terms based on certain conditions.
*
* This function should be used whenever you need to load more than one term
* from the database. Terms are loaded into memory and will not require
* database access if loaded again during the same page request.
*
* @see entity_load_multiple()
* @see \Drupal\Core\Entity\Query\EntityQueryInterface
*
* @deprecated in Drupal 8.x, will be removed before Drupal 9.0.
* Use \Drupal\taxonomy\Entity\Term::loadMultiple().
*
* @param array $tids
* (optional) An array of entity IDs. If omitted, all entities are loaded.
*
* @return array
* An array of taxonomy term entities, indexed by tid. When no results are
* found, an empty array is returned.
*/
function taxonomy_term_load_multiple(array $tids = NULL) {
return Term::loadMultiple($tids);
}
/**
* Loads multiple taxonomy vocabularies based on certain conditions.
*
* This function should be used whenever you need to load more than one
* vocabulary from the database. Terms are loaded into memory and will not
* require database access if loaded again during the same page request.
*
* @see entity_load_multiple()
*
* @deprecated in Drupal 8.x, will be removed before Drupal 9.0.
* Use \Drupal\taxonomy\Entity\Vocabulary::loadMultiple().
*
* @param array $vids
* (optional) An array of entity IDs. If omitted, all entities are loaded.
*
* @return array
* An array of vocabulary objects, indexed by vid.
*/
function taxonomy_vocabulary_load_multiple(array $vids = NULL) {
return Vocabulary::loadMultiple($vids);
}
/**
* Return the taxonomy vocabulary entity matching a vocabulary ID.
*
* @deprecated in Drupal 8.x, will be removed before Drupal 9.0.
* Use \Drupal\taxonomy\Entity\Vocabulary::load().
*
* @param int $vid
* The vocabulary's ID.
*
* @return \Drupal\taxonomy\Entity\Vocabulary|null
* The taxonomy vocabulary entity, if exists, NULL otherwise. Results are
* statically cached.
*/
function taxonomy_vocabulary_load($vid) {
return Vocabulary::load($vid);
}
/**
* Return the taxonomy term entity matching a term ID.
*
* @deprecated in Drupal 8.x, will be removed before Drupal 9.0.
* Use \Drupal\taxonomy\Entity\Term::load().
*
* @param $tid
* A term's ID
*
* @return \Drupal\taxonomy\Entity\Term|null
* A taxonomy term entity, or NULL if the term was not found. Results are
* statically cached.
*/
function taxonomy_term_load($tid) {
if (!is_numeric($tid)) {
return NULL;
}
return Term::load($tid);
}
/**
* Implodes a list of tags of a certain vocabulary into a string.
*
* @see \Drupal\Component\Utility\Tags::explode()
*/
function taxonomy_implode_tags($tags, $vid = NULL) {
$typed_tags = [];
foreach ($tags as $tag) {
// Extract terms belonging to the vocabulary in question.
if (!isset($vid) || $tag->bundle() == $vid) {
// Make sure we have a completed loaded taxonomy term.
if ($tag instanceof EntityInterface && $label = $tag->label()) {
// Commas and quotes in tag names are special cases, so encode 'em.
$typed_tags[] = Tags::encode($label);
}
}
}
return implode(', ', $typed_tags);
}
/**
* Title callback for term pages.
*
* @param \Drupal\taxonomy\Entity\Term $term
* A taxonomy term entity.
*
* @return
* The term name to be used as the page title.
*/
function taxonomy_term_title(Term $term) {
return $term->getName();
}
/**
* @defgroup taxonomy_index Taxonomy indexing
* @{
* Functions to maintain taxonomy indexing.
*
* Taxonomy uses default field storage to store canonical relationships
* between terms and fieldable entities. However its most common use case
* requires listing all content associated with a term or group of terms
* sorted by creation date. To avoid slow queries due to joining across
* multiple node and field tables with various conditions and order by criteria,
* we maintain a denormalized table with all relationships between terms,
* published nodes and common sort criteria such as status, sticky and created.
* When using other field storage engines or alternative methods of
* denormalizing this data you should set the
* taxonomy.settings:maintain_index_table to '0' to avoid unnecessary writes in
* SQL.
*/
/**
* Implements hook_ENTITY_TYPE_insert() for node entities.
*/
function taxonomy_node_insert(EntityInterface $node) {
// Add taxonomy index entries for the node.
taxonomy_build_node_index($node);
}
/**
* Builds and inserts taxonomy index entries for a given node.
*
* The index lists all terms that are related to a given node entity, and is
* therefore maintained at the entity level.
*
* @param \Drupal\node\Entity\Node $node
* The node entity.
*/
function taxonomy_build_node_index($node) {
// We maintain a denormalized table of term/node relationships, containing
// only data for current, published nodes.
if (!\Drupal::config('taxonomy.settings')->get('maintain_index_table') || !(\Drupal::entityManager()->getStorage('node') instanceof SqlContentEntityStorage)) {
return;
}
$status = $node->isPublished();
$sticky = (int) $node->isSticky();
// We only maintain the taxonomy index for published nodes.
if ($status && $node->isDefaultRevision()) {
// Collect a unique list of all the term IDs from all node fields.
$tid_all = [];
$entity_reference_class = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem';
foreach ($node->getFieldDefinitions() as $field) {
$field_name = $field->getName();
$class = $field->getItemDefinition()->getClass();
$is_entity_reference_class = ($class === $entity_reference_class) || is_subclass_of($class, $entity_reference_class);
if ($is_entity_reference_class && $field->getSetting('target_type') == 'taxonomy_term') {
foreach ($node->getTranslationLanguages() as $language) {
foreach ($node->getTranslation($language->getId())->$field_name as $item) {
if (!$item->isEmpty()) {
$tid_all[$item->target_id] = $item->target_id;
}
}
}
}
}
// Insert index entries for all the node's terms.
if (!empty($tid_all)) {
foreach ($tid_all as $tid) {
db_merge('taxonomy_index')
->key(['nid' => $node->id(), 'tid' => $tid, 'status' => $node->isPublished()])
->fields(['sticky' => $sticky, 'created' => $node->getCreatedTime()])
->execute();
}
}
}
}
/**
* Implements hook_ENTITY_TYPE_update() for node entities.
*/
function taxonomy_node_update(EntityInterface $node) {
// If we're not dealing with the default revision of the node, do not make any
// change to the taxonomy index.
if (!$node->isDefaultRevision()) {
return;
}
taxonomy_delete_node_index($node);
taxonomy_build_node_index($node);
}
/**
* Implements hook_ENTITY_TYPE_predelete() for node entities.
*/
function taxonomy_node_predelete(EntityInterface $node) {
// Clean up the {taxonomy_index} table when nodes are deleted.
taxonomy_delete_node_index($node);
}
/**
* Deletes taxonomy index entries for a given node.
*
* @param \Drupal\Core\Entity\EntityInterface $node
* The node entity.
*/
function taxonomy_delete_node_index(EntityInterface $node) {
if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) {
db_delete('taxonomy_index')->condition('nid', $node->id())->execute();
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for taxonomy_term entities.
*/
function taxonomy_taxonomy_term_delete(Term $term) {
if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) {
// Clean up the {taxonomy_index} table when terms are deleted.
db_delete('taxonomy_index')->condition('tid', $term->id())->execute();
}
}
/**
* @} End of "defgroup taxonomy_index".
*/

View file

@ -0,0 +1,9 @@
administer taxonomy:
title: 'Administer vocabularies and terms'
access taxonomy overview:
title: 'Access the taxonomy vocabulary overview page'
description: 'Get an overview of all taxonomy vocabularies.'
permission_callbacks:
- Drupal\taxonomy\TaxonomyPermissions::permissions

View file

@ -0,0 +1,127 @@
<?php
/**
* @file
* Post update functions for Taxonomy.
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\views\ViewExecutable;
/**
* Clear caches due to updated taxonomy entity views data.
*/
function taxonomy_post_update_clear_views_data_cache() {
// An empty update will flush caches.
}
/**
* Clear entity_bundle_field_definitions cache for new parent field settings.
*/
function taxonomy_post_update_clear_entity_bundle_field_definitions_cache() {
// An empty update will flush caches.
}
/**
* Add a 'published' = TRUE filter for all Taxonomy term views and converts
* existing ones that were using the 'content_translation_status' field.
*/
function taxonomy_post_update_handle_publishing_status_addition_in_views(&$sandbox = NULL) {
$definition_update_manager = \Drupal::entityDefinitionUpdateManager();
$entity_type = $definition_update_manager->getEntityType('taxonomy_term');
$published_key = $entity_type->getKey('published');
$status_filter = [
'id' => 'status',
'table' => 'taxonomy_term_field_data',
'field' => $published_key,
'relationship' => 'none',
'group_type' => 'group',
'admin_label' => '',
'operator' => '=',
'value' => '1',
'group' => 1,
'exposed' => FALSE,
'expose' => [
'operator_id' => '',
'label' => '',
'description' => '',
'use_operator' => FALSE,
'operator' => '',
'identifier' => '',
'required' => FALSE,
'remember' => FALSE,
'multiple' => FALSE,
'remember_roles' => [
'authenticated' => 'authenticated',
'anonymous' => '0',
'administrator' => '0',
],
],
'is_grouped' => FALSE,
'group_info' => [
'label' => '',
'description' => '',
'identifier' => '',
'optional' => TRUE,
'widget' => 'select',
'multiple' => FALSE,
'remember' => FALSE,
'default_group' => 'All',
'default_group_multiple' => [],
'group_items' => [],
],
'entity_type' => 'taxonomy_term',
'entity_field' => $published_key,
'plugin_id' => 'boolean',
];
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function ($view) use ($published_key, $status_filter) {
/** @var \Drupal\views\ViewEntityInterface $view */
// Only alter taxonomy term views.
if ($view->get('base_table') !== 'taxonomy_term_field_data') {
return FALSE;
}
$displays = $view->get('display');
foreach ($displays as $display_name => &$display) {
// Update any existing 'content_translation_status fields.
$fields = isset($display['display_options']['fields']) ? $display['display_options']['fields'] : [];
foreach ($fields as $id => $field) {
if (isset($field['field']) && $field['field'] == 'content_translation_status') {
$fields[$id]['field'] = $published_key;
}
}
$display['display_options']['fields'] = $fields;
// Update any existing 'content_translation_status sorts.
$sorts = isset($display['display_options']['sorts']) ? $display['display_options']['sorts'] : [];
foreach ($sorts as $id => $sort) {
if (isset($sort['field']) && $sort['field'] == 'content_translation_status') {
$sorts[$id]['field'] = $published_key;
}
}
$display['display_options']['sorts'] = $sorts;
// Update any existing 'content_translation_status' filters or add a new
// one if necessary.
$filters = isset($display['display_options']['filters']) ? $display['display_options']['filters'] : [];
$has_status_filter = FALSE;
foreach ($filters as $id => $filter) {
if (isset($filter['field']) && $filter['field'] == 'content_translation_status') {
$filters[$id]['field'] = $published_key;
$has_status_filter = TRUE;
}
}
if (!$has_status_filter) {
$status_filter['id'] = ViewExecutable::generateHandlerId($published_key, $filters);
$filters[$status_filter['id']] = $status_filter;
}
$display['display_options']['filters'] = $filters;
}
$view->set('display', $displays);
return TRUE;
});
}

View file

@ -0,0 +1,39 @@
entity.taxonomy_term.add_form:
path: '/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/add'
defaults:
_controller: '\Drupal\taxonomy\Controller\TaxonomyController::addForm'
_title: 'Add term'
requirements:
_entity_create_access: 'taxonomy_term:{taxonomy_vocabulary}'
entity.taxonomy_term.edit_form:
path: '/taxonomy/term/{taxonomy_term}/edit'
defaults:
_entity_form: 'taxonomy_term.default'
_title: 'Edit term'
options:
_admin_route: TRUE
requirements:
_entity_access: 'taxonomy_term.update'
taxonomy_term: \d+
entity.taxonomy_term.delete_form:
path: '/taxonomy/term/{taxonomy_term}/delete'
defaults:
_entity_form: 'taxonomy_term.delete'
_title: 'Delete term'
options:
_admin_route: TRUE
requirements:
_entity_access: 'taxonomy_term.delete'
taxonomy_term: \d+
entity.taxonomy_term.canonical:
path: '/taxonomy/term/{taxonomy_term}'
defaults:
_entity_view: 'taxonomy_term.full'
_title: 'Taxonomy term'
_title_callback: '\Drupal\taxonomy\Controller\TaxonomyController::termTitle'
requirements:
_entity_access: 'taxonomy_term.view'
taxonomy_term: \d+

View file

@ -0,0 +1,6 @@
services:
taxonomy_term.breadcrumb:
class: Drupal\taxonomy\TermBreadcrumbBuilder
arguments: ['@entity.manager']
tags:
- { name: breadcrumb_builder, priority: 1002 }

View file

@ -0,0 +1,192 @@
<?php
/**
* @file
* Builds placeholder replacement tokens for taxonomy terms and vocabularies.
*/
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Implements hook_token_info().
*/
function taxonomy_token_info() {
$types['term'] = [
'name' => t("Taxonomy terms"),
'description' => t("Tokens related to taxonomy terms."),
'needs-data' => 'term',
];
$types['vocabulary'] = [
'name' => t("Vocabularies"),
'description' => t("Tokens related to taxonomy vocabularies."),
'needs-data' => 'vocabulary',
];
// Taxonomy term related variables.
$term['tid'] = [
'name' => t("Term ID"),
'description' => t("The unique ID of the taxonomy term."),
];
$term['name'] = [
'name' => t("Name"),
'description' => t("The name of the taxonomy term."),
];
$term['description'] = [
'name' => t("Description"),
'description' => t("The optional description of the taxonomy term."),
];
$term['node-count'] = [
'name' => t("Node count"),
'description' => t("The number of nodes tagged with the taxonomy term."),
];
$term['url'] = [
'name' => t("URL"),
'description' => t("The URL of the taxonomy term."),
];
// Taxonomy vocabulary related variables.
$vocabulary['vid'] = [
'name' => t("Vocabulary ID"),
'description' => t("The unique ID of the taxonomy vocabulary."),
];
$vocabulary['name'] = [
'name' => t("Name"),
'description' => t("The name of the taxonomy vocabulary."),
];
$vocabulary['description'] = [
'name' => t("Description"),
'description' => t("The optional description of the taxonomy vocabulary."),
];
$vocabulary['node-count'] = [
'name' => t("Node count"),
'description' => t("The number of nodes tagged with terms belonging to the taxonomy vocabulary."),
];
$vocabulary['term-count'] = [
'name' => t("Term count"),
'description' => t("The number of terms belonging to the taxonomy vocabulary."),
];
// Chained tokens for taxonomies
$term['vocabulary'] = [
'name' => t("Vocabulary"),
'description' => t("The vocabulary the taxonomy term belongs to."),
'type' => 'vocabulary',
];
$term['parent'] = [
'name' => t("Parent term"),
'description' => t("The parent term of the taxonomy term, if one exists."),
'type' => 'term',
];
return [
'types' => $types,
'tokens' => [
'term' => $term,
'vocabulary' => $vocabulary,
],
];
}
/**
* Implements hook_tokens().
*/
function taxonomy_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
$token_service = \Drupal::token();
$replacements = [];
$taxonomy_storage = \Drupal::entityManager()->getStorage('taxonomy_term');
if ($type == 'term' && !empty($data['term'])) {
$term = $data['term'];
foreach ($tokens as $name => $original) {
switch ($name) {
case 'tid':
$replacements[$original] = $term->id();
break;
case 'name':
$replacements[$original] = $term->getName();
break;
case 'description':
// "processed" returns a \Drupal\Component\Render\MarkupInterface via
// check_markup().
$replacements[$original] = $term->description->processed;
break;
case 'url':
$replacements[$original] = $term->url('canonical', ['absolute' => TRUE]);
break;
case 'node-count':
$query = db_select('taxonomy_index');
$query->condition('tid', $term->id());
$query->addTag('term_node_count');
$count = $query->countQuery()->execute()->fetchField();
$replacements[$original] = $count;
break;
case 'vocabulary':
$vocabulary = Vocabulary::load($term->bundle());
$bubbleable_metadata->addCacheableDependency($vocabulary);
$replacements[$original] = $vocabulary->label();
break;
case 'parent':
if ($parents = $taxonomy_storage->loadParents($term->id())) {
$parent = array_pop($parents);
$bubbleable_metadata->addCacheableDependency($parent);
$replacements[$original] = $parent->getName();
}
break;
}
}
if ($vocabulary_tokens = $token_service->findWithPrefix($tokens, 'vocabulary')) {
$vocabulary = Vocabulary::load($term->bundle());
$replacements += $token_service->generate('vocabulary', $vocabulary_tokens, ['vocabulary' => $vocabulary], $options, $bubbleable_metadata);
}
if (($vocabulary_tokens = $token_service->findWithPrefix($tokens, 'parent')) && $parents = $taxonomy_storage->loadParents($term->id())) {
$parent = array_pop($parents);
$replacements += $token_service->generate('term', $vocabulary_tokens, ['term' => $parent], $options, $bubbleable_metadata);
}
}
elseif ($type == 'vocabulary' && !empty($data['vocabulary'])) {
$vocabulary = $data['vocabulary'];
foreach ($tokens as $name => $original) {
switch ($name) {
case 'vid':
$replacements[$original] = $vocabulary->id();
break;
case 'name':
$replacements[$original] = $vocabulary->label();
break;
case 'description':
$build = ['#markup' => $vocabulary->getDescription()];
// @todo Fix in https://www.drupal.org/node/2577827
$replacements[$original] = \Drupal::service('renderer')->renderPlain($build);
break;
case 'term-count':
$replacements[$original] = \Drupal::entityQuery('taxonomy_term')
->condition('vid', $vocabulary->id())
->addTag('vocabulary_term_count')
->count()
->execute();
break;
case 'node-count':
$replacements[$original] = $taxonomy_storage->nodeCount($vocabulary->id());
break;
}
}
}
return $replacements;
}

View file

@ -0,0 +1,85 @@
<?php
/**
* @file
* Provides views data for taxonomy.module.
*/
use Drupal\field\FieldStorageConfigInterface;
/**
* Implements hook_views_data_alter().
*/
function taxonomy_views_data_alter(&$data) {
$data['node_field_data']['term_node_tid'] = [
'title' => t('Taxonomy terms on node'),
'help' => t('Relate nodes to taxonomy terms, specifying which vocabulary or vocabularies to use. This relationship will cause duplicated records if there are multiple terms.'),
'relationship' => [
'id' => 'node_term_data',
'label' => t('term'),
'base' => 'taxonomy_term_field_data',
],
'field' => [
'title' => t('All taxonomy terms'),
'help' => t('Display all taxonomy terms associated with a node from specified vocabularies.'),
'id' => 'taxonomy_index_tid',
'no group by' => TRUE,
'click sortable' => FALSE,
],
];
$data['node_field_data']['term_node_tid_depth'] = [
'help' => t('Display content if it has the selected taxonomy terms, or children of the selected terms. Due to additional complexity, this has fewer options than the versions without depth.'),
'real field' => 'nid',
'argument' => [
'title' => t('Has taxonomy term ID (with depth)'),
'id' => 'taxonomy_index_tid_depth',
'accept depth modifier' => TRUE,
],
'filter' => [
'title' => t('Has taxonomy terms (with depth)'),
'id' => 'taxonomy_index_tid_depth',
],
];
$data['node_field_data']['term_node_tid_depth_modifier'] = [
'title' => t('Has taxonomy term ID depth modifier'),
'help' => t('Allows the "depth" for Taxonomy: Term ID (with depth) to be modified via an additional contextual filter value.'),
'argument' => [
'id' => 'taxonomy_index_tid_depth_modifier',
],
];
}
/**
* Implements hook_field_views_data_alter().
*
* Views integration for entity reference fields which reference taxonomy terms.
* Adds a term relationship to the default field data.
*
* @see views_field_default_views_data()
*/
function taxonomy_field_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) {
if ($field_storage->getType() == 'entity_reference' && $field_storage->getSetting('target_type') == 'taxonomy_term') {
foreach ($data as $table_name => $table_data) {
foreach ($table_data as $field_name => $field_data) {
if (isset($field_data['filter']) && $field_name != 'delta') {
$data[$table_name][$field_name]['filter']['id'] = 'taxonomy_index_tid';
}
}
}
}
}
/**
* Implements hook_views_plugins_argument_validator_alter().
*
* Extend the generic entity argument validator.
*
* @see \Drupal\views\Plugin\views\argument_validator\Entity
*/
function taxonomy_views_plugins_argument_validator_alter(array &$plugins) {
$plugins['entity:taxonomy_term']['title'] = t('Taxonomy term ID');
$plugins['entity:taxonomy_term']['class'] = 'Drupal\taxonomy\Plugin\views\argument_validator\Term';
$plugins['entity:taxonomy_term']['provider'] = 'taxonomy';
}

View file

@ -0,0 +1,35 @@
{#
/**
* @file
* Default theme implementation to display a taxonomy term.
*
* Available variables:
* - url: URL of the current term.
* - name: Name of the current term.
* - content: Items for the content of the term (fields and description).
* Use 'content' to print them all, or print a subset such as
* 'content.description'. Use the following code to exclude the
* printing of a given child element:
* @code
* {{ content|without('description') }}
* @endcode
* - attributes: HTML attributes for the wrapper.
* - page: Flag for the full page state.
* - term: The taxonomy term entity, including:
* - id: The ID of the taxonomy term.
* - bundle: Machine name of the current vocabulary.
* - view_mode: View mode, e.g. 'full', 'teaser', etc.
*
* @see template_preprocess_taxonomy_term()
*
* @ingroup themeable
*/
#}
<div{{ attributes }}>
{{ title_prefix }}
{% if not page %}
<h2><a href="{{ url }}">{{ name }}</a></h2>
{% endif %}
{{ title_suffix }}
{{ content }}
</div>

View file

@ -0,0 +1,32 @@
<?php
/**
* @file
* Contains database additions to drupal-8.filled.standard.php.gz for testing
* the upgrade path of https://www.drupal.org/project/drupal/issues/2981887.
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();
$view_file = __DIR__ . '/views.view.test_taxonomy_term_view_with_content_translation_status.yml';
$view_with_cts_config = Yaml::decode(file_get_contents($view_file));
$view_file = __DIR__ . '/views.view.test_taxonomy_term_view_without_content_translation_status.yml';
$view_without_cts_config = Yaml::decode(file_get_contents($view_file));
$connection->insert('config')
->fields(['collection', 'name', 'data'])
->values([
'collection' => '',
'name' => 'views.view.test_taxonomy_term_view_with_content_translation_status',
'data' => serialize($view_with_cts_config),
])
->values([
'collection' => '',
'name' => 'views.view.test_taxonomy_term_view_without_content_translation_status',
'data' => serialize($view_without_cts_config),
])
->execute();

View file

@ -0,0 +1,250 @@
langcode: en
status: true
dependencies:
module:
- taxonomy
- user
id: test_taxonomy_term_view_with_content_translation_status
label: 'Test taxonomy term view with content translation status'
module: views
description: ''
tag: ''
base_table: taxonomy_term_field_data
base_field: tid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
offset: 0
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
name:
id: name
table: taxonomy_term_field_data
field: name
entity_type: taxonomy_term
entity_field: name
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
type: string
settings:
link_to_entity: true
plugin_id: term_name
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
convert_spaces: false
content_translation_status:
id: content_translation_status
table: taxonomy_term_field_data
field: content_translation_status
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: boolean
settings:
format: true-false
format_custom_true: ''
format_custom_false: ''
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: taxonomy_term
entity_field: content_translation_status
plugin_id: field
filters:
content_translation_status:
id: content_translation_status
table: taxonomy_term_field_data
field: content_translation_status
relationship: none
group_type: group
admin_label: ''
operator: '='
value: All
group: 1
exposed: true
expose:
operator_id: ''
label: 'Translation status'
description: ''
use_operator: false
operator: content_translation_status_op
identifier: content_translation_status
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: taxonomy_term
entity_field: content_translation_status
plugin_id: boolean
sorts:
content_translation_status:
id: content_translation_status
table: taxonomy_term_field_data
field: content_translation_status
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: taxonomy_term
entity_field: content_translation_status
plugin_id: standard
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- user.permissions
tags: { }

View file

@ -0,0 +1,128 @@
langcode: en
status: true
dependencies:
module:
- taxonomy
- user
id: test_taxonomy_term_view_without_content_translation_status
label: 'Test taxonomy term view without content translation status'
module: views
description: ''
tag: ''
base_table: taxonomy_term_field_data
base_field: tid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
offset: 0
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
name:
id: name
table: taxonomy_term_field_data
field: name
entity_type: taxonomy_term
entity_field: name
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
type: string
settings:
link_to_entity: true
plugin_id: term_name
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
convert_spaces: false
filters: { }
sorts: { }
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- user.permissions
tags: { }

View file

@ -0,0 +1,7 @@
taxonomy.vocabulary.*.third_party.taxonomy_crud:
type: mapping
label: 'Schema for taxonomy_crud module additions to vocabulary entity'
mapping:
foo:
type: string
label: 'Label for foo'

View file

@ -0,0 +1,8 @@
name: 'Taxonomy CRUD tests'
type: module
description: 'Provides 3rd party settings for vocabulary.'
package: Testing
version: VERSION
core: 8.x
dependencies:
- drupal:taxonomy

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Provides hook implementations for testing purposes.
*/
use Drupal\taxonomy\VocabularyInterface;
/**
* Implements hook_ENTITY_TYPE_presave() for taxonomy_vocabulary entities.
*/
function taxonomy_crud_taxonomy_vocabulary_presave(VocabularyInterface $vocabulary) {
$vocabulary->setThirdPartySetting('taxonomy_crud', 'foo', 'bar');
}

Some files were not shown because too many files have changed in this diff Show more