mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/structural_pages.git
synced 2026-05-05 08:40:41 -03:00
- field_site_section migrado de entity_reference para dynamic_entity_reference, permitindo referenciar taxonomy_term, user ou event como domínio raiz da página - field_parent_page restrito a content_page nodes apenas (removidos taxonomy_term/user) - Update hooks 10011–10013: migração de schema/config, limpeza de referências inválidas e correção do tipo do storage field_parent_page para DER - SectionBreadcrumbBuilder reescrito para usar field_site_section como raiz - StructuralPagesMenuBlock: queries de domínio via DB direto (evita bug do entity query com especificador target_type em campos DER) - Formulário content_page: pre-fill de field_site_section via query params ?domain_type/domain_id ou ?parent_section_type/parent_id; atualiza widget #default_value diretamente para refletir o valor na renderização inicial - Parâmetro de URL parent_type renomeado para parent_section_type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
307 lines
8.7 KiB
PHP
307 lines
8.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Drupal\structural_pages\Breadcrumb;
|
|
|
|
use Drupal\Core\Breadcrumb\Breadcrumb;
|
|
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
|
|
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
|
use Drupal\Core\Link;
|
|
use Drupal\Core\Routing\RouteMatchInterface;
|
|
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
|
use Drupal\node\NodeInterface;
|
|
use Drupal\structural_pages\ParentEntityHandler\ParentEntityHandlerManagerInterface;
|
|
|
|
/**
|
|
* Provides a breadcrumb builder for section_page and content_page content types.
|
|
*/
|
|
class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface
|
|
{
|
|
|
|
use StringTranslationTrait;
|
|
|
|
/**
|
|
* The entity type manager.
|
|
*
|
|
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
|
*/
|
|
protected EntityTypeManagerInterface $entityTypeManager;
|
|
|
|
/**
|
|
* The parent entity handler manager.
|
|
*
|
|
* @var \Drupal\structural_pages\ParentEntityHandler\ParentEntityHandlerManagerInterface
|
|
*/
|
|
protected ParentEntityHandlerManagerInterface $handlerManager;
|
|
|
|
/**
|
|
* Content types that this builder applies to.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected array $applicableBundles = ['section_page', 'content_page'];
|
|
|
|
/**
|
|
* Constructs a SectionBreadcrumbBuilder object.
|
|
*
|
|
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
|
* The entity type manager.
|
|
* @param \Drupal\structural_pages\ParentEntityHandler\ParentEntityHandlerManagerInterface $handler_manager
|
|
* The parent entity handler manager.
|
|
*/
|
|
public function __construct(
|
|
EntityTypeManagerInterface $entity_type_manager,
|
|
ParentEntityHandlerManagerInterface $handler_manager,
|
|
) {
|
|
$this->entityTypeManager = $entity_type_manager;
|
|
$this->handlerManager = $handler_manager;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function applies(RouteMatchInterface $route_match): bool
|
|
{
|
|
$node = $route_match->getParameter('node');
|
|
|
|
if ($node instanceof NodeInterface) {
|
|
return in_array($node->bundle(), $this->applicableBundles, TRUE);
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function build(RouteMatchInterface $route_match): Breadcrumb
|
|
{
|
|
$breadcrumb = new Breadcrumb();
|
|
$breadcrumb->addCacheContexts(['route']);
|
|
|
|
$node = $route_match->getParameter('node');
|
|
|
|
if (!$node instanceof NodeInterface) {
|
|
return $breadcrumb;
|
|
}
|
|
|
|
$breadcrumb->addCacheableDependency($node);
|
|
|
|
// Add "Home" as first item.
|
|
$breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
|
|
|
|
// Resolve domain root from field_site_section of this node or its ancestors.
|
|
$domain = $this->resolveDomain($node);
|
|
|
|
if ($domain) {
|
|
if ($domain['type'] === 'taxonomy_term') {
|
|
$this->addTaxonomyBreadcrumbs($breadcrumb, $domain['entity']->id());
|
|
}
|
|
else {
|
|
$this->handlerManager->buildBreadcrumbForEntity($breadcrumb, $domain['entity']);
|
|
}
|
|
}
|
|
|
|
// For content_page, add node parent hierarchy.
|
|
if ($node->bundle() === 'content_page') {
|
|
$this->addParentBreadcrumbs($breadcrumb, $node);
|
|
}
|
|
|
|
// Gavias notech theme bug bypass: the theme requires an empty element
|
|
// before the title to correctly iterate and render '/' separators.
|
|
$breadcrumb->addLink(Link::createFromRoute('', '<none>'));
|
|
// Append the active page title, as the theme's title resolver fails for nodes.
|
|
$breadcrumb->addLink(Link::createFromRoute($node->getTitle(), '<none>'));
|
|
|
|
return $breadcrumb;
|
|
}
|
|
|
|
/**
|
|
* Resolves the domain root for the given node.
|
|
*
|
|
* Walks up the field_parent_page chain to the topmost content_page, then
|
|
* reads field_site_section (DER) to get the domain entity (taxonomy_term,
|
|
* user, event, …).
|
|
*
|
|
* @param \Drupal\node\NodeInterface $node
|
|
* Any node (section_page or content_page).
|
|
*
|
|
* @return array{type: string, entity: \Drupal\Core\Entity\EntityInterface}|null
|
|
* Array with 'type' and 'entity', or NULL if domain cannot be resolved.
|
|
*/
|
|
protected function resolveDomain(NodeInterface $node): ?array
|
|
{
|
|
// For section_page or any node with field_site_section, read it directly.
|
|
if ($node->hasField('field_site_section') && !$node->get('field_site_section')->isEmpty()) {
|
|
$root = $node;
|
|
|
|
// For content_page, walk up to the topmost node to get the canonical
|
|
// field_site_section (which should be the same on all nodes in the chain).
|
|
if ($node->bundle() === 'content_page') {
|
|
$root = $this->getRootContentPage($node);
|
|
}
|
|
|
|
if ($root->hasField('field_site_section') && !$root->get('field_site_section')->isEmpty()) {
|
|
$fss = $root->get('field_site_section')->first();
|
|
$domain_type = $fss->target_type ?? 'taxonomy_term';
|
|
$domain_id = $fss->target_id;
|
|
|
|
$domain_entity = $this->entityTypeManager->getStorage($domain_type)->load($domain_id);
|
|
if ($domain_entity) {
|
|
$breadcrumb_arg = &$this; // used only to add cache dependency below.
|
|
return ['type' => $domain_type, 'entity' => $domain_entity];
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Walks up field_parent_page to find the topmost content_page.
|
|
*
|
|
* @param \Drupal\node\NodeInterface $node
|
|
* The starting node.
|
|
*
|
|
* @return \Drupal\node\NodeInterface
|
|
* The root content_page (or $node itself if no parent chain found).
|
|
*/
|
|
protected function getRootContentPage(NodeInterface $node): NodeInterface
|
|
{
|
|
$visited = [];
|
|
$current = $node;
|
|
|
|
while (TRUE) {
|
|
if (isset($visited[$current->id()])) {
|
|
break;
|
|
}
|
|
$visited[$current->id()] = TRUE;
|
|
|
|
if (!$current->hasField('field_parent_page') || $current->get('field_parent_page')->isEmpty()) {
|
|
return $current;
|
|
}
|
|
|
|
$pf = $current->get('field_parent_page')->first();
|
|
$parent_id = $pf->target_id ?? NULL;
|
|
|
|
if (!$parent_id) {
|
|
return $current;
|
|
}
|
|
|
|
$parent = $this->entityTypeManager->getStorage('node')->load($parent_id);
|
|
if (!$parent instanceof NodeInterface) {
|
|
return $current;
|
|
}
|
|
|
|
$current = $parent;
|
|
}
|
|
|
|
return $node;
|
|
}
|
|
|
|
/**
|
|
* Adds breadcrumbs based on taxonomy hierarchy.
|
|
*
|
|
* @param \Drupal\Core\Breadcrumb\Breadcrumb $breadcrumb
|
|
* The breadcrumb object.
|
|
* @param int|string $term_id
|
|
* The taxonomy term ID.
|
|
*/
|
|
protected function addTaxonomyBreadcrumbs(Breadcrumb $breadcrumb, int|string $term_id): void
|
|
{
|
|
$term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
|
|
$ancestors = $term_storage->loadAllParents($term_id);
|
|
$ancestors = array_reverse($ancestors);
|
|
|
|
foreach ($ancestors as $ancestor) {
|
|
$breadcrumb->addCacheableDependency($ancestor);
|
|
$breadcrumb->addLink($ancestor->toLink());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds breadcrumbs based on content_page parent hierarchy.
|
|
*
|
|
* Only adds node parents to the breadcrumb, not user/group/taxonomy
|
|
* which are handled separately.
|
|
*
|
|
* @param \Drupal\Core\Breadcrumb\Breadcrumb $breadcrumb
|
|
* The breadcrumb object.
|
|
* @param \Drupal\node\NodeInterface $node
|
|
* The current node.
|
|
*/
|
|
protected function addParentBreadcrumbs(Breadcrumb $breadcrumb, NodeInterface $node): void
|
|
{
|
|
$parents = $this->getNodeParents($node);
|
|
$parents = array_reverse($parents);
|
|
|
|
foreach ($parents as $parent) {
|
|
$breadcrumb->addCacheableDependency($parent);
|
|
$breadcrumb->addLink($parent->toLink());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all node parents of a content_page.
|
|
*
|
|
* Stops when encountering a non-node parent (user, group, taxonomy_term).
|
|
*
|
|
* @param \Drupal\node\NodeInterface $node
|
|
* The node.
|
|
*
|
|
* @return \Drupal\node\NodeInterface[]
|
|
* Array of parent nodes, from closest to farthest.
|
|
*/
|
|
protected function getNodeParents(NodeInterface $node): array
|
|
{
|
|
$parents = [];
|
|
$visited = [];
|
|
$current = $node;
|
|
|
|
while (
|
|
$current instanceof NodeInterface &&
|
|
$current->hasField('field_parent_page') &&
|
|
!$current->get('field_parent_page')->isEmpty()
|
|
) {
|
|
|
|
$parent_field = $current->get('field_parent_page')->first();
|
|
if (!$parent_field) {
|
|
break;
|
|
}
|
|
|
|
$parent_entity_type = $parent_field->target_type ?? NULL;
|
|
$parent_id = $parent_field->target_id ?? NULL;
|
|
|
|
if (!$parent_entity_type || !$parent_id) {
|
|
break;
|
|
}
|
|
|
|
// Only continue if parent is a node.
|
|
if ($parent_entity_type !== 'node') {
|
|
break;
|
|
}
|
|
|
|
// Avoid infinite loops.
|
|
if (isset($visited[$parent_id])) {
|
|
break;
|
|
}
|
|
$visited[$parent_id] = TRUE;
|
|
|
|
$parent = $this->entityTypeManager
|
|
->getStorage('node')
|
|
->load($parent_id);
|
|
|
|
if (!$parent instanceof NodeInterface) {
|
|
break;
|
|
}
|
|
|
|
$parents[] = $parent;
|
|
$current = $parent;
|
|
}
|
|
|
|
return $parents;
|
|
}
|
|
|
|
}
|