276 lines
10 KiB
PHP
276 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Backend\Breadcrumb;
|
|
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use Psr\Log\LoggerInterface;
|
|
use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;
|
|
use TYPO3\CMS\Backend\Module\ModuleInterface;
|
|
use TYPO3\CMS\Backend\Module\ModuleResolver;
|
|
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
|
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
|
use TYPO3\CMS\Core\Domain\RecordInterface;
|
|
use TYPO3\CMS\Core\Imaging\IconFactory;
|
|
use TYPO3\CMS\Core\Imaging\IconSize;
|
|
use TYPO3\CMS\Core\Localization\LanguageService;
|
|
|
|
/**
|
|
* Breadcrumb provider for TYPO3 records (pages, content elements, etc.).
|
|
*
|
|
* Generates breadcrumb trails based on page rootlines and record hierarchies.
|
|
*
|
|
* @internal This class is not part of TYPO3's public API.
|
|
*/
|
|
final readonly class RecordBreadcrumbProvider implements BreadcrumbProviderInterface
|
|
{
|
|
public function __construct(
|
|
private IconFactory $iconFactory,
|
|
private ModuleResolver $moduleResolver,
|
|
private UriBuilder $uriBuilder,
|
|
private LoggerInterface $logger,
|
|
) {}
|
|
|
|
public function supports(?BreadcrumbContext $context): bool
|
|
{
|
|
return $context?->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'];
|
|
}
|
|
}
|