mainContext instanceof RecordInterface; } public function generate(?BreadcrumbContext $context, ?ServerRequestInterface $request): array { if ($context === null || !$context->mainContext instanceof RecordInterface) { return []; } $record = $context->mainContext; $breadcrumb = []; $currentModule = $this->moduleResolver->resolveModule($request); $showRootline = $this->shouldShowRootline($currentModule); $targetModule = $currentModule !== null ? $this->extractRouteIdentifier($request, $currentModule) : $this->getTargetModule(); // Add module hierarchy (for third-level modules, this includes parent modules) if ($currentModule !== null) { $breadcrumb = array_merge($breadcrumb, $this->buildModuleHierarchy($currentModule, $request, $showRootline)); } else { $breadcrumb[] = new BreadcrumbNode( identifier: '0', label: (string)$GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], icon: 'apps-pagetree-root', url: (string)$this->uriBuilder->buildUriFromRoute($targetModule, $showRootline ? ['id' => '0'] : []), ); } // Add page rootline if applicable if ($showRootline) { $breadcrumb = array_merge($breadcrumb, $this->buildRootline($record, $targetModule)); } // Add the current record $breadcrumb[] = $this->buildRecordNode($record, $targetModule); return $breadcrumb; } public function getPriority(): int { return 10; } /** * Returns the target module identifier for navigation. */ private function getTargetModule(): string { // Default to web_layout for page-based navigation return 'web_layout'; } /** * Builds the page rootline for a record. * * @return BreadcrumbNode[] */ private function buildRootline(RecordInterface $record, string $targetModule): array { $breadcrumb = []; $pid = $record->getPid(); try { $rootline = BackendUtility::BEgetRootLine($pid); if ($rootline === []) { return []; } // Remove the site root (already added as first node) array_pop($rootline); ksort($rootline); foreach ($rootline as $item) { if (!is_array($item) || !isset($item['uid'])) { continue; } try { $icon = $this->iconFactory->getIconForRecord('pages', $item, IconSize::SMALL); $breadcrumb[] = new BreadcrumbNode( identifier: (string)$item['uid'], label: BackendUtility::cropToTitleLength($item['title'] ?? ''), icon: $icon->getIdentifier(), iconOverlay: $icon->getOverlayIcon()?->getIdentifier(), url: (string)$this->uriBuilder->buildUriFromRoute($targetModule, ['id' => $item['uid']]), ); } catch (\Exception $e) { $this->logger->warning( 'Failed to create breadcrumb node for page', ['uid' => $item['uid'], 'exception' => $e->getMessage()] ); } } } catch (\Exception $e) { $this->logger->warning( 'Failed to build rootline for record', ['table' => $record->getMainType(), 'uid' => $record->getUid(), 'exception' => $e->getMessage()] ); } return $breadcrumb; } /** * Builds a breadcrumb node for the current record. */ private function buildRecordNode(RecordInterface $record, string $targetModule): BreadcrumbNode { try { $icon = $this->iconFactory->getIconForRecord( $record->getMainType(), $record->getRawRecord()?->toArray(), IconSize::SMALL ); $recordTitle = BackendUtility::getRecordTitle($record->getMainType(), $record->getRawRecord()?->toArray()); return new BreadcrumbNode( identifier: (string)$record->getUid(), label: BackendUtility::cropToTitleLength($recordTitle), icon: $icon->getIdentifier(), iconOverlay: $icon->getOverlayIcon()?->getIdentifier(), url: $record->getMainType() === 'pages' ? (string)$this->uriBuilder->buildUriFromRoute($targetModule, ['id' => (string)$record->getUid()]) : null, ); } catch (\Exception $e) { $this->logger->error( 'Failed to create breadcrumb node for record', ['table' => $record->getMainType(), 'uid' => $record->getUid(), 'exception' => $e->getMessage()] ); // Return a minimal fallback node return new BreadcrumbNode( identifier: (string)$record->getUid(), label: $record->getMainType() . ' [' . $record->getUid() . ']', ); } } /** * Builds the module hierarchy including parent modules. * * For third-level modules, this returns [parent, current]. * For second-level modules, this returns [current]. * For standalone modules, this returns [current]. * * @return BreadcrumbNode[] */ private function buildModuleHierarchy(ModuleInterface $currentModule, ?ServerRequestInterface $request, bool $showRootline): array { $modules = []; $moduleChain = []; // Build chain from current to root $module = $currentModule; while ($module !== null) { $moduleChain[] = $module; $module = $module->getParentModule(); } // Reverse to get root-to-current order and skip the top-level parent (main menu item) $moduleChain = array_reverse($moduleChain); // Skip the first item if we have more than one (first is the main menu container like "web") if (count($moduleChain) > 1) { array_shift($moduleChain); } // Build breadcrumb nodes for each module in the chain foreach ($moduleChain as $index => $module) { $isLastModule = $index === count($moduleChain) - 1; // For the last module (current), use the full route identifier to preserve route/action // For parent modules, use base module identifier $routeIdentifier = $isLastModule ? $this->extractRouteIdentifier($request, $module) : $module->getIdentifier(); $modules[] = new BreadcrumbNode( identifier: $module->getIdentifier(), label: $this->getLanguageService()->sL($module->getTitle()), icon: $module->getIconIdentifier(), url: (string)$this->uriBuilder->buildUriFromRoute($routeIdentifier, $showRootline ? ['id' => '0'] : []), forceShowIcon: true, ); } return $modules; } /** * Determines if the rootline should be shown based on the current module. * * Modules using the page tree navigation component typically support page-based navigation. */ private function shouldShowRootline(?ModuleInterface $currentModule): bool { // @todo This is quite implicit, but using the page-tree-element as navigation component // signals that the current module can handle ?id= as a page parameter. return $currentModule === null || $currentModule->getNavigationComponent() === '@typo3/backend/tree/page-tree-element'; } /** * Extracts the route identifier from the current request. * * This returns the full route identifier (e.g., 'manage_search_index.Administration_externalDocuments') * to preserve sub-routes and actions in breadcrumb navigation. * * @return string The route identifier or module identifier as fallback */ private function extractRouteIdentifier(?ServerRequestInterface $request, ModuleInterface $module): string { // Try to get the full route identifier from routing attribute if ($request !== null && ($routeResult = $request->getAttribute('routing')) !== null && ($route = $routeResult->getRoute()) !== null && !empty(($routeIdentifier = $route->getOption('_identifier'))) ) { return $routeIdentifier; } // Fallback to module identifier return $module->getIdentifier(); } private function getLanguageService(): LanguageService { return $GLOBALS['LANG']; } }