taxonomy_overview-1.0.1/src/Controller/TagsOverviewSingleController.php
src/Controller/TagsOverviewSingleController.php
<?php
namespace Drupal\taxonomy_overview\Controller;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\node\NodeInterface;
use Drupal\paragraphs\Entity\Paragraph;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Controller for tags overview: lists taxonomy terms and related info.
*
* Loads terms for a vocabulary (or a single term), finds related paragraphs
* and nodes, translations and builds a table with operations.
*/
class TagsOverviewSingleController implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The current route match service.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The language manager service.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The form builder service.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The vocabulary ID (bundle) being inspected.
*
* @var string|null
*/
private $vocabularyId;
/**
* The taxonomy term context (if any).
*
* @var \Drupal\taxonomy\Entity\Term|null
*/
private $taxonomyTerm;
/**
* Constructs a TagsOverviewSingleController.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager service.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
*/
public function __construct(RouteMatchInterface $route_match, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, FormBuilderInterface $form_builder, ConfigFactoryInterface $config_factory) {
$this->routeMatch = $route_match;
$this->entityTypeManager = $entity_type_manager;
$this->languageManager = $language_manager;
$this->formBuilder = $form_builder;
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = new static(
$container->get('current_route_match'),
$container->get('entity_type.manager'),
$container->get('language_manager'),
$container->get('form_builder'),
$container->get('config.factory')
);
// string_translation service used by StringTranslationTrait.
$instance->setStringTranslation($container->get('string_translation'));
return $instance;
}
/**
* Builds the overview page content for taxonomy terms.
*
* The request may contain filters:
* - search: search string in term name
* - sort_by: sort field (name, count, paragraph_count, translations,
* node_count_content_type).
* - sort_order: asc|desc
* - random_bg: 1 to enable (unused in logic; removed unused var)
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array
* A render array for the page.
*/
public function content(Request $request) {
// Aggregation containers.
$term_node_pages_content_types = [];
$term_paragraph_by_content_type = [];
$paragraph_node_used_bundle = [];
$term_links_translatable = [];
$term_duplicates_count = [];
$paragraph_node_used = [];
$term_translations = [];
$term_node_counts = [];
$paragraph_bundle = [];
$paragraph_fields = [];
$paragraph_count = [];
$paragraph_ids = [];
$node_fields = [];
$term_names = [];
// Route parameters.
$this->vocabularyId = $this->routeMatch->getParameter('taxonomy_vocabulary');
$this->taxonomyTerm = $this->routeMatch->getParameter('taxonomy_term');
if ($this->taxonomyTerm instanceof Term) {
$this->vocabularyId = $this->taxonomyTerm->bundle();
}
// Discover fields referencing this vocabulary.
$entity_fields = $this->checkEntityReferenceVocabulary($this->vocabularyId);
if (isset($entity_fields['node'])) {
foreach ($entity_fields['node'] as $value) {
$node_fields = array_merge(array_values($value), $node_fields);
}
$node_fields = array_unique($node_fields);
foreach ($entity_fields['node'] as $value) {
$paragraph_fields = array_merge(array_values($value), $paragraph_fields);
}
$paragraph_fields = array_unique($paragraph_fields);
}
$paragraph_types = $entity_fields['paragraph'] ?? [];
$search_term = $request->query->get('search', '');
$sort_by = $request->query->get('sort_by', 'name');
$sort_order = $request->query->get('sort_order', 'desc');
// Build taxonomy term query via storage.
$term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
$query = $term_storage->getQuery()
->accessCheck(FALSE)
->condition('vid', $this->vocabularyId);
if (!empty($search_term)) {
$query->condition('name', '%' . $search_term . '%', 'LIKE');
}
if ($this->taxonomyTerm) {
$query->condition('tid', $this->taxonomyTerm->id());
}
if ($sort_by === 'name') {
$query->sort('name', $sort_order);
}
$term_ids = $query->execute();
$terms = $term_storage->loadMultiple($term_ids);
// Prepare paragraph storage and other storages used below.
$paragraph_storage = $this->entityTypeManager->getStorage('paragraph');
$node_storage = $this->entityTypeManager->getStorage('node');
foreach ($terms as $term) {
$term_name = $term->getName();
$term_id = $term->id();
$paragraph_bundle[$term_id] = [];
$paragraph_node_used[$term_id] = [];
$paragraph_node_used_bundle[$term_id] = [];
$term_paragraph_by_content_type[$term_id] = [];
$term_duplicates_count[$term_name] = $term_duplicates_count[$term_name] ?? 0;
$term_links_translatable[$term_id] = $this->getTranslationsLinks($term);
$translations = $this->getTranslations($term);
$term_translations[$term_id] = implode(', ', $translations);
if ($paragraph_types) {
// For each paragraph type, count paragraphs that reference this term.
foreach ($paragraph_types as $field_names) {
foreach ($field_names as $field_name) {
$paragraph_count[$term_id] = $paragraph_count[$term_id] ?? 0;
$paragraph_ids[$term_id] = $paragraph_ids[$term_id] ?? [];
$n_paragraphs = $paragraph_storage->getQuery()
->accessCheck(FALSE)
->condition('status', 1)
->condition($field_name, $term_id)
->execute();
$paragraph_count[$term_id] += count($n_paragraphs);
$paragraph_ids[$term_id] = array_merge($paragraph_ids[$term_id], $n_paragraphs);
}
}
if (!empty($paragraph_ids[$term_id])) {
foreach ($paragraph_ids[$term_id] as $pid) {
$p = $paragraph_storage->load($pid);
if ($p) {
$data_used = $this->getParentEntityNode($p);
$paragraph_bundle[$term_id][] = $p->bundle();
if ($data_used !== NULL) {
$paragraph_node_used[$term_id][] = $data_used['id'] ?? '';
$paragraph_node_used_bundle[$term_id][] = $data_used['bundle'] ?? '';
}
}
}
}
}
// Count nodes that reference the term through
// any node field we discovered.
$node_count_arr = [];
if (!empty($node_fields)) {
$node_query = $node_storage->getQuery()->accessCheck(FALSE);
$or_group = $node_query->orConditionGroup();
foreach ($node_fields as $f) {
$or_group->condition($f, $term_id, 'in');
}
$node_count_arr = $node_query->condition($or_group)->execute();
}
$term_node_counts[$term_id] = count(array_unique(array_filter(array_merge($paragraph_node_used[$term_id] ?? [], $node_count_arr))));
$term_node_pages_content_types[$term_id] = implode(
', ',
array_unique(
array_merge($paragraph_node_used_bundle[$term_id] ?? [], $this->getContentTypesByTaxonomyTerm($term_id))
)
);
if ($term_node_pages_content_types[$term_id] == '') {
unset($term_node_pages_content_types[$term_id]);
}
$term_duplicates_count[$term_name]++;
$term_names[$term_name][] = $term->id();
}
// Sorting on arrays of Term objects by various criteria.
if ($sort_by === 'count') {
uasort($terms, function ($a, $b) use ($term_node_counts, $sort_order) {
$a_count = $term_node_counts[$a->id()] ?? 0;
$b_count = $term_node_counts[$b->id()] ?? 0;
return ($sort_order === 'asc') ? ($a_count <=> $b_count) : ($b_count <=> $a_count);
});
}
elseif ($sort_by === 'node_count_content_type') {
uasort($terms, function ($a, $b) use ($term_node_pages_content_types, $sort_order) {
$a_count = $term_node_pages_content_types[$a->id()] ?? '';
$b_count = $term_node_pages_content_types[$b->id()] ?? '';
return ($sort_order === 'asc') ? ($a_count <=> $b_count) : ($b_count <=> $a_count);
});
}
elseif ($sort_by === 'paragraph_count') {
uasort($terms, function ($a, $b) use ($paragraph_count, $sort_order) {
$a_count = $paragraph_count[$a->id()] ?? 0;
$b_count = $paragraph_count[$b->id()] ?? 0;
return ($sort_order === 'asc') ? ($a_count <=> $b_count) : ($b_count <=> $a_count);
});
}
elseif ($sort_by === 'translations') {
uasort($terms, function ($a, $b) use ($term_translations, $sort_order) {
$a_translations = str_replace('Default: ', '', $term_translations[$a->id()] ?? '');
$b_translations = str_replace('Default: ', '', $term_translations[$b->id()] ?? '');
return ($sort_order === 'asc') ? strcasecmp($a_translations, $b_translations) : strcasecmp($b_translations, $a_translations);
});
}
$build['fieldset'] = [
'#type' => "fieldset",
'#title' => 'Term Overview',
];
// Define table rows.
foreach ($terms as $term) {
$term_name = $term->getName();
$term_id = $term->id();
$build['fieldset']['term_' . $term->id()] = [
'#type' => 'container',
'#attributes' => ['class' => ['tag-card']],
'tid' => [
'#markup' => $this->t('<div><span class="label">Term ID:</span> @id</div>', [
'@id' => $term->id(),
]),
],
'name' => [
'#markup' => $this->t('<div><span class="label">Name:</span> @name</div>', [
'@name' => Markup::create(implode(', ', $term_links_translatable[$term_id] ?? [])),
]),
],
'nodes' => [
'#markup' => $this->t('<div><span class="label">Nº Nodes:</span> @count</div>', [
'@count' => $term_node_counts[$term_id] ?? 0,
]),
],
'content_type' => [
'#markup' => $this->t('<div><span class="label">Content Type:</span> @type</div>', [
'@type' => $term_node_pages_content_types[$term_id] ?? '--',
]),
],
'paragraphs' => [
'#markup' => $this->t('<div><span class="label">Nº Paragraph:</span> @count</div>', [
'@count' => $paragraph_count[$term_id] ?? 0,
]),
],
'bundle' => [
'#markup' => $this->t('<div><span class="label">Paragraph Bundle:</span> @bundle</div>', [
'@bundle' => !empty($paragraph_bundle[$term_id]) ? implode(', ', $paragraph_bundle[$term_id]) : '--',
]),
],
'translations' => [
'#markup' => $this->t('<div><span class="label">Translations:</span> @langs</div>', [
'@langs' => $term_translations[$term_id] ?? '',
]),
],
'fields_used' => [
'#markup' => $this->t('<div><span class="label">Fields Used:</span> @fields</div>', [
'@fields' => implode(', ', $node_fields),
]),
],
];
}
// Attach simple CSS.
$build['#attached']['html_head'][] = [
[
'#tag' => 'style',
'#value' => '
.tags-summary { margin-bottom: 20px; font-size: 15px; }
.tag-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
background: #fafafa;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
.tag-card div { margin-bottom: 6px; }
.tag-card .label {
font-weight: 800;
color: #444;
display: inline-block;
min-width: 150px;
}
',
],
'tags_report_styles',
];
return $build;
}
/**
* Get distinct content types that reference the given taxonomy term.
*
* @param int $term_id
* The taxonomy term ID.
*
* @return array
* An array of content type machine names.
*/
private function getContentTypesByTaxonomyTerm($term_id) {
$database = Database::getConnection();
$query = $database->select('node_field_data', 'nfd')
->distinct()
->fields('nfd', ['type']);
$query->innerJoin('taxonomy_index', 'ti', 'nfd.nid = ti.nid');
$content_types = $query->condition('ti.tid', $term_id)->execute()->fetchCol();
return $content_types ?: [];
}
/**
* Find the parent entity node for an entity (Node or Paragraph).
*
* Traverses paragraph parent chain until it finds a node.
*
* @param mixed $entity
* The entity or paragraph instance.
*
* @return array|null
* An array containing 'bundle' and 'id' of the node or NULL if none.
*/
private function getParentEntityNode($entity) {
if ($entity instanceof NodeInterface) {
return [
'bundle' => $entity->bundle(),
'id' => $entity->id(),
];
}
if ($entity instanceof Paragraph) {
$parent = $entity->getParentEntity();
if ($parent !== NULL) {
return $this->getParentEntityNode($parent);
}
}
return NULL;
}
/**
* Check all fields to see which ones reference the given vocabulary.
*
* @param string $vocabularyId
* The vocabulary machine name.
*
* @return array
* An array keyed by entity type listing field names and paragraph mapping.
*/
public function checkEntityReferenceVocabulary($vocabularyId) {
$arr_data = [];
$field_storage_configs = $this->entityTypeManager
->getStorage('field_storage_config')
->loadMultiple();
foreach ($field_storage_configs as $field_storage_config) {
$type = $field_storage_config->getType();
if ($type === 'entity_reference' || $type === 'entity_reference_revisions') {
$settings = $field_storage_config->getSettings();
if (isset($settings['target_type']) && $settings['target_type'] === 'taxonomy_term') {
$field_configs = $this->entityTypeManager
->getStorage('field_config')
->loadByProperties(['field_name' => $field_storage_config->getName()]);
foreach ($field_configs as $field_config) {
$field_settings = $field_config->getSetting('handler_settings');
if (isset($field_settings['target_bundles'][$vocabularyId])) {
$entity_type = $field_config->getTargetEntityTypeId();
$arr_data[$entity_type][$field_config->getTargetBundle()][] = $field_config->getName();
}
}
}
}
}
return $arr_data;
}
/**
* Check whether term translations are enabled for the vocabulary.
*
* @return bool
* TRUE if vocabulary term translation is enabled.
*/
public function isVocabularyTermsTranslationEnabled() : bool {
if (empty($this->vocabularyId)) {
return FALSE;
}
$vocabulary_storage = $this->entityTypeManager->getStorage('taxonomy_vocabulary');
$vocabulary = $vocabulary_storage->load($this->vocabularyId);
if ($vocabulary instanceof Vocabulary) {
$config = $this->configFactory->get('language.content_settings.taxonomy_term.' . $this->vocabularyId);
if ($config) {
$data = $config->getRawData();
return !empty($data['third_party_settings']['content_translation']['enabled']);
}
}
return FALSE;
}
/**
* Returns term links for each translation (language: <a href="...">name</a>).
*
* @param \Drupal\taxonomy\Entity\Term $term
* The term entity.
*
* @return array
* An array of strings ready for Markup containing language code and link.
*/
public function getTranslationsLinks(Term $term) {
$languages = $this->languageManager->getLanguages();
$term_links_translatable = [];
foreach ($languages as $language) {
$langcode = $language->getId();
if ($term->hasTranslation($langcode)) {
$translation = $term->getTranslation($langcode);
$term_links_translatable[] = $langcode . ': <a href="' . $translation->toUrl()->toString() . '">' . $translation->getName() . '</a>';
}
}
return $term_links_translatable;
}
/**
* Get language codes for translations available for the term.
*
* @param \Drupal\taxonomy\Entity\Term $term
* The term entity.
*
* @return array
* Array of language codes.
*/
public function getTranslations(Term $term) {
$languages = $this->languageManager->getLanguages();
$translations = [];
$translations[$term->langcode->value] = $term->langcode->value;
foreach ($languages as $language) {
$langcode = $language->getId();
if (!isset($translations[$langcode]) && $term->hasTranslation($langcode)) {
$translations[] = $langcode;
}
}
return $translations;
}
}
