Files
structural_pages/src/Breadcrumb/SectionBreadcrumbBuilder.php
Quintino A. G. Souza 88b9605408 Rename module site_structure → structural_pages and vocabulary site_section → site_sections
Renames the module from site_structure to structural_pages and pluralizes
the taxonomy vocabulary machine name from site_section to site_sections,
updating all config, PHP, translations, and documentation references
while preserving field_site_section, clears_site_section, and $site_section_id.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 08:10:32 -03:00

291 lines
7.9 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>'));
// Determine the context type for content_page.
$context = $this->getParentContext($node);
if ($context) {
// Use the handler manager to build breadcrumbs for the context entity.
$this->handlerManager->buildBreadcrumbForEntity($breadcrumb, $context['entity']);
}
else {
// No parent context, check for site section directly.
if ($node->hasField('field_site_section') && !$node->get('field_site_section')->isEmpty()) {
$term_id = $node->get('field_site_section')->target_id;
$this->addTaxonomyBreadcrumbs($breadcrumb, $term_id);
}
}
// For content_page, add node parent hierarchy.
if ($node->bundle() === 'content_page') {
$this->addParentBreadcrumbs($breadcrumb, $node);
}
return $breadcrumb;
}
/**
* Gets the root parent context for a content_page.
*
* Traverses up the parent chain to find the root context (user, group, or taxonomy).
*
* @param \Drupal\node\NodeInterface $node
* The content_page node.
*
* @return array|null
* An array with 'type' and 'entity' keys, or NULL if no context.
*/
protected function getParentContext(NodeInterface $node): ?array {
if ($node->bundle() !== 'content_page') {
return NULL;
}
$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;
}
// Avoid infinite loops.
$visit_key = $parent_entity_type . ':' . $parent_id;
if (isset($visited[$visit_key])) {
break;
}
$visited[$visit_key] = TRUE;
$parent = $this->entityTypeManager
->getStorage($parent_entity_type)
->load($parent_id);
if (!$parent) {
break;
}
// If parent is user or group, that's our context.
if (in_array($parent_entity_type, ['user', 'group'])) {
return [
'type' => $parent_entity_type,
'entity' => $parent,
];
}
// If parent is taxonomy_term, that's our context (handled via site_sections).
if ($parent_entity_type === 'taxonomy_term') {
return [
'type' => 'taxonomy_term',
'entity' => $parent,
];
}
// If parent is a node, continue traversing.
if ($parent instanceof NodeInterface) {
$current = $parent;
}
else {
break;
}
}
return NULL;
}
/**
* 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;
}
}