Adiciona formatter de lista hierárquica para o vocabulário MSC 2020

MscTermListFormatter agrupa os termos selecionados por categoria pai,
exibindo o pai como item de primeiro nível e os filhos como sub-lista.
Pais não selecionados diretamente são carregados automaticamente para
servir de cabeçalho do grupo. Inclui opção "Link para a entidade
referenciada" para linkar os rótulos às páginas dos termos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 15:04:39 -03:00
parent 7631ef0484
commit 706c2bd737

View File

@@ -0,0 +1,212 @@
<?php
namespace Drupal\site_tools_msc_2020\Plugin\Field\FieldFormatter;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Formatter que exibe termos MSC 2020 como lista hierárquica aninhada.
*
* Os termos selecionados são agrupados pela categoria pai. O pai aparece
* como item de primeiro nível e os filhos como sub-lista dentro dele.
* Opcionalmente, os rótulos são linkados para a página do termo.
*
* @FieldFormatter(
* id = "msc_term_list",
* label = @Translation("MSC 2020 — Lista hierárquica"),
* field_types = {
* "entity_reference"
* }
* )
*/
class MscTermListFormatter extends FormatterBase {
public function __construct(
$plugin_id,
$plugin_definition,
FieldDefinitionInterface $field_definition,
array $settings,
$label,
$view_mode,
array $third_party_settings,
protected EntityTypeManagerInterface $entityTypeManager,
) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['label'],
$configuration['view_mode'],
$configuration['third_party_settings'],
$container->get('entity_type.manager'),
);
}
public static function isApplicable(FieldDefinitionInterface $field_definition): bool {
$storage = $field_definition->getFieldStorageDefinition();
if ($storage->getSetting('target_type') !== 'taxonomy_term') {
return FALSE;
}
$handler_settings = $field_definition->getSetting('handler_settings');
return !empty($handler_settings['target_bundles']['msc_2020']);
}
public static function defaultSettings(): array {
return ['link_to_entity' => FALSE] + parent::defaultSettings();
}
public function settingsForm(array $form, FormStateInterface $form_state): array {
$elements = parent::settingsForm($form, $form_state);
$elements['link_to_entity'] = [
'#type' => 'checkbox',
'#title' => $this->t('Link para a entidade referenciada'),
'#default_value' => $this->getSetting('link_to_entity'),
];
return $elements;
}
public function settingsSummary(): array {
$summary = parent::settingsSummary();
$summary[] = $this->getSetting('link_to_entity')
? $this->t('Link para a entidade referenciada')
: $this->t('Sem link para a entidade referenciada');
return $summary;
}
public function viewElements(FieldItemListInterface $items, $langcode): array {
$tids = [];
foreach ($items as $item) {
if ($item instanceof EntityReferenceItem && !empty($item->target_id)) {
$tids[] = (int) $item->target_id;
}
}
if (empty($tids)) {
return [];
}
$link = (bool) $this->getSetting('link_to_entity');
$storage = $this->entityTypeManager->getStorage('taxonomy_term');
$terms = $storage->loadMultiple($tids);
// Separa pais e filhos; coleta códigos de pais ausentes.
$parents_by_code = [];
$children_by_code = [];
$missing_codes = [];
foreach ($terms as $term) {
$code = $term->get('field_msc_code')->value;
if (strlen($code) === 2) {
$parents_by_code[$code] = $term;
}
else {
$parent_code = substr($code, 0, 2);
$children_by_code[$parent_code][] = $term;
if (!isset($parents_by_code[$parent_code])) {
$missing_codes[$parent_code] = $parent_code;
}
}
}
// Carrega pais que não foram selecionados mas são necessários para agrupar.
if (!empty($missing_codes)) {
$parent_tids = $storage->getQuery()
->condition('vid', 'msc_2020')
->condition('field_msc_code', array_values($missing_codes), 'IN')
->accessCheck(FALSE)
->execute();
foreach ($storage->loadMultiple($parent_tids) as $term) {
$code = $term->get('field_msc_code')->value;
$parents_by_code[$code] = $term;
}
}
// Ordena grupos por código.
$all_codes = array_unique(
array_merge(array_keys($parents_by_code), array_keys($children_by_code))
);
sort($all_codes);
// Monta o render array como lista aninhada.
$build = [
'#prefix' => '<ul class="msc-terms-list">',
'#suffix' => '</ul>',
];
foreach ($all_codes as $i => $code) {
$parent = $parents_by_code[$code] ?? NULL;
$children = $children_by_code[$code] ?? [];
usort($children, fn($a, $b) => strcmp(
$a->get('field_msc_code')->value,
$b->get('field_msc_code')->value,
));
$build[$i] = [
'#prefix' => '<li class="msc-terms-list__parent">',
'#suffix' => '</li>',
'label' => $this->termLabel($parent, $code, $link),
];
if (!empty($children)) {
$build[$i]['children'] = [
'#prefix' => '<ul class="msc-terms-list__children">',
'#suffix' => '</ul>',
];
foreach ($children as $j => $child) {
$child_code = $child->get('field_msc_code')->value;
$build[$i]['children'][$j] = [
'#prefix' => '<li class="msc-terms-list__child">',
'#suffix' => '</li>',
] + $this->termLabel($child, $child_code, $link);
}
}
}
return [$build];
}
/**
* Retorna um render array para o rótulo do termo, com ou sem link.
*
* Quando o termo pai não está na seleção (apenas agrupa filhos), é exibido
* sem link mesmo que a opção esteja ativa, pois o pai não foi selecionado.
*
* @param \Drupal\taxonomy\TermInterface|null $term
* Termo a exibir, ou NULL se o pai não foi carregado.
* @param string $code
* Código MSC usado como fallback quando $term é NULL.
* @param bool $link
* Se TRUE e $term não é NULL, envolve o rótulo num link.
*/
protected function termLabel($term, string $code, bool $link): array {
if ($term === NULL) {
return ['#markup' => Html::escape($code)];
}
$text = Html::escape($code . ' — ' . $term->label());
if ($link && $term->access('view')) {
return [
'#type' => 'link',
'#title' => $code . ' — ' . $term->label(),
'#url' => $term->toUrl(),
];
}
return ['#markup' => $text];
}
}