taxonomy_overview-1.0.1/src/Controller/TagsOverviewController.php

src/Controller/TagsOverviewController.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\Link;
use Drupal\Core\Render\Markup;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
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 TagsOverviewController 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 TagsOverviewController.
   *
   * @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 = [];
    $rows = [];

    // 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);
    $total_count = count($terms);

    // 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))
        )
      );
      $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);
      });
    }

    // Define table headers with sorting
    // links (split lines to keep width small).
    $header = [
      [
        'data' => $this->t('Term ID'),
        'specifier' => 'tid',
      ],
      [
        'data' => $this->t('Name'),
        'specifier' => 'name',
      ],
      [
        'data' => $this->t('Nº Nodes'),
        'specifier' => 'count',
      ],
      [
        'data' => $this->t('Content Type'),
        'specifier' => 'node_count_content_type',
      ],
      [
        'data' => $this->t('Nº Paragraph'),
        'specifier' => 'paragraph_count',
      ],
      [
        'data' => $this->t('Paragraph Bundle'),
        'specifier' => 'bundles',
      ],
      [
        'data' => $this->t('Translations'),
        'specifier' => 'translations',
      ],
      [
        'data' => $this->t('Operations'),
      ],
    ];

    // Convert header titles to sorting links when needed.
    foreach ($header as &$column) {
      if (isset($column['specifier']) && isset($column['field'])) {
        $is_current = ($sort_by === $column['field']);
        $new_order = $is_current && $sort_order === 'asc' ? 'desc' : 'asc';
        $column['data'] = Link::fromTextAndUrl(
          $column['data'],
          Url::fromRoute('<current>', [], [
            'query' => [
              'search' => $search_term,
              'sort_by' => $column['field'],
              'sort_order' => $new_order,
            ],
          ])
        )->toString();
      }
    }
    unset($column);

    // Define table rows.
    foreach ($terms as $term) {
      $term_name = $term->getName();
      $term_id = $term->id();

      $edit_links = $this->getLinks($term);

      $rows[] = [
        'data' => [
          'tid' => $term->id(),
          'name' => Markup::create(implode('<br>', $term_links_translatable[$term_id] ?? [])),
          'count' => $term_node_counts[$term_id] ?? 0,
          'node_count_content_type' => $term_node_pages_content_types[$term_id] ?? '',
          'paragraph_count' => $paragraph_count[$term_id] ?? '',
          'bundles' => implode(',', $paragraph_bundle[$term_id] ?? []),
          'translations' => $term_translations[$term_id] ?? '',
          'operations' => [
            'data' => [
              '#type' => 'operations',
              '#links' => $edit_links,
            ],
          ],
        ],
      ];
    }

    // Build render array.
    $build = [
      '#type' => 'container',
      '#attributes' => [
        'id' => 'tags-overview-container',
      ],
      'fieldset' => [
        '#type' => 'fieldset',
        '#title' => $this->t('Taxonomy Overview'),
        'search_form' => [
          '#type' => 'form',
          '#method' => 'get',
          'search_input' => [
            '#type' => 'textfield',
            '#title' => $this->t('Search'),
            '#value' => $search_term,
            '#attributes' => [
              'name' => 'search',
              'placeholder' => $this->t('Enter tag name'),
              'style' => 'display:flex',
            ],
          ],
          'sort_by' => [
            '#type' => 'hidden',
            '#value' => $sort_by,
            '#attributes' => [
              'name' => 'sort_by',
            ],
          ],
          'sort_order' => [
            '#type' => 'hidden',
            '#value' => $sort_order,
            '#attributes' => [
              'name' => 'sort_order',
            ],
          ],
          'submit_button' => [
            '#type' => 'submit',
            '#value' => $this->t('Search'),
          ],
        ],
        'result_count' => [
          '#markup' => $this->t(
            '<h6>Total Tags: @count</h6><h6>Fields: <small>@node_fields</small></h6>',
            [
              '@count' => $total_count,
              '@node_fields' => implode(',', $node_fields),
            ]
          ),
        ],
      ],
      'term_table' => [
        '#type' => 'table',
        '#header' => $header,
        '#rows' => $rows,
        '#empty' => $this->t('No terms found.'),
        '#attributes' => [
          'class' => ['table-auto-break'],
        ],
      ],
      'pager' => [
        '#type' => 'pager',
        '#element' => 0,
      ],
    ];

    $build['#attached']['library'][] = 'taxonomy_overview/taxonomy_overview.table_style';

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

  /**
   * Build operation links for a term (edit, delete, translation actions).
   *
   * @param \Drupal\taxonomy\Entity\Term $term
   *   The term entity.
   *
   * @return array
   *   An array of operation links suitable for '#links' in
   *   an operations element.
   */
  public function getLinks(Term $term) {
    $languages = $this->languageManager->getLanguages();

    $edit_links = [
      'edit' => [
        'title' => $this->t('Edit'),
        'url' => Url::fromRoute('entity.taxonomy_term.edit_form', ['taxonomy_term' => $term->id()]),
      ],
      'delete' => [
        'title' => $this->t('Delete'),
        'url' => Url::fromRoute('entity.taxonomy_term.delete_form', ['taxonomy_term' => $term->id()]),
      ],
    ];

    if ($this->isVocabularyTermsTranslationEnabled()) {
      foreach ($languages as $language) {
        $langcode = $language->getId();
        if ($term->hasTranslation($langcode)) {
          // Edit translation link.
          $edit_links['edit_' . $langcode] = [
            'title' => $this->t('Edit @lang', ['@lang' => strtoupper($langcode)]),
            'url' => Url::fromRoute('entity.taxonomy_term.edit_form', [
              'taxonomy_term' => $term->id(),
              'langcode' => $langcode,
            ]),
          ];
          // Delete translation link (specify language on the link if needed).
          $edit_links['delete_' . $langcode] = [
            'title' => $this->t('Delete @lang', ['@lang' => strtoupper($langcode)]),
            'url' => Url::fromRoute('entity.taxonomy_term.delete_form', [
              'taxonomy_term' => $term->id(),
            ], [
              'language' => $this->languageManager->getLanguage($langcode),
            ]),
          ];
        }
        else {
          // Add translation link.
          $edit_links['create_' . $langcode] = [
            'title' => $this->t('Create @lang', ['@lang' => strtoupper($langcode)]),
            'url' => Url::fromRoute('entity.taxonomy_term.content_translation_add', [
              'taxonomy_term' => $term->id(),
              'source' => $term->langcode->value,
              'target' => $langcode,
            ], [
              'absolute' => FALSE,
            ]),
          ];
        }
      }
    }

    return $edit_links;
  }

  /**
   * 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;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc