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