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'), '')); // 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('', '')); // Append the active page title, as the theme's title resolver fails for nodes. $breadcrumb->addLink(Link::createFromRoute($node->getTitle(), '')); 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; } }