TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -0,0 +1,238 @@
<?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\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Backend\Module\ModuleRegistry;
use TYPO3\CMS\Backend\Routing\Router;
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;
/**
* Debug backend routes, including module routes, AJAX routes and regular routes.
* Similar to Symfony's debug:router command.
* @internal only for development purposes
*/
#[AsCommand('debug:backend:routes', 'Debugging: List all registered backend routes (only for development purpose)')]
#[AsNonSchedulableCommand]
class DebugBackendRoutesCommand extends Command
{
public function __construct(
private readonly Router $router,
private readonly ModuleRegistry $moduleRegistry,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addOption(
'json',
null,
InputOption::VALUE_NONE,
'Output routes in JSON format'
)
->addOption(
'filter',
'f',
InputOption::VALUE_REQUIRED,
'Filter routes by name (supports partial matching)'
)
->addOption(
'limit',
'l',
InputOption::VALUE_REQUIRED,
'Limit routes by type: ajax, module, or route'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$jsonOutput = $input->getOption('json');
$filter = $input->getOption('filter');
$limit = $input->getOption('limit');
// Validate limit option
if ($limit !== null) {
$limit = strtolower($limit);
if (!in_array($limit, ['ajax', 'module', 'route'], true)) {
$io->error('Invalid limit type. Valid options are: ajax, module, route');
return Command::FAILURE;
}
}
// Collect all routes
$routes = $this->collectAllRoutes($filter, $limit);
if (empty($routes)) {
$messages = [];
if ($filter) {
$messages[] = 'filter: ' . $filter;
}
if ($limit) {
$messages[] = 'type: ' . $limit;
}
if ($messages) {
$io->warning('No routes found matching ' . implode(', ', $messages));
} else {
$io->warning('No routes found.');
}
return Command::SUCCESS;
}
// Sort routes by name
ksort($routes);
if ($jsonOutput) {
$this->outputJson($routes, $output);
} else {
$this->outputTable($routes, $io, $output);
}
return Command::SUCCESS;
}
/**
* Collect all routes from Router (including AJAX routes) and Module routes
*
* @return array<string, array{name: string, method: string, path: string, target: string, type: string, options: array}>
*/
private function collectAllRoutes(?string $filter, ?string $limitType): array
{
$routes = [];
// Build a set of module identifiers for quick lookup
$moduleIdentifiers = [];
foreach ($this->moduleRegistry->getModules() as $module) {
if ($module->hasParentModule() || $module->isStandalone()) {
$moduleIdentifiers[$module->getIdentifier()] = true;
}
}
// Get all routes from Router (includes regular routes, AJAX routes, and module routes)
// Note: Routes can be either TYPO3 Route or Symfony Route objects
foreach ($this->router->getRoutes() as $routeName => $route) {
if ($filter && !str_contains((string)$routeName, $filter)) {
continue;
}
// Determine route type
$type = 'Route';
if (str_starts_with((string)$routeName, 'ajax_')) {
$type = 'Ajax';
} elseif (isset($moduleIdentifiers[(string)$routeName])) {
$type = 'Module';
}
// Apply limit filter if specified
if ($limitType !== null) {
$typeNormalized = strtolower($type);
if ($typeNormalized !== $limitType) {
continue;
}
}
// Get methods - works for both Symfony and TYPO3 Route objects
$methods = $route->getMethods();
$methodString = empty($methods) ? 'ANY' : implode('|', $methods);
// Get options - works for both route types
$options = method_exists($route, 'getOptions') ? $route->getOptions() : [];
$target = $options['target'] ?? $options['_controller'] ?? '-';
$routes[$routeName] = [
'name' => (string)$routeName,
'method' => $methodString,
'path' => $route->getPath(),
'target' => $target,
'type' => $type,
'options' => $options,
];
}
return $routes;
}
/**
* Output routes as JSON
*/
private function outputJson(array $routes, OutputInterface $output): void
{
$jsonData = [];
foreach ($routes as $route) {
$jsonData[] = [
'name' => $route['name'],
'method' => $route['method'],
'path' => $route['path'],
'target' => $route['target'],
'type' => $route['type'],
'options' => $route['options'],
];
}
$output->writeln((string)json_encode($jsonData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
/**
* Output routes as formatted table (similar to Symfony's debug:router)
*/
private function outputTable(array $routes, SymfonyStyle $io, OutputInterface $output): void
{
$table = new Table($output);
$table->setHeaders(['Name', 'Method', 'Path', 'Target', 'Type']);
$rows = [];
foreach ($routes as $route) {
$rows[] = [
$route['name'],
$route['method'],
$route['path'],
$this->formatTarget($route['target']),
$route['type'],
];
}
$table->setRows($rows);
$table->render();
$io->newLine();
$io->writeln(sprintf('<info>%d</info> routes found', count($routes)));
}
/**
* Format the target for display (shorten class names)
*/
private function formatTarget(string $target): string
{
// Shorten TYPO3 class names for better readability
$target = str_replace('TYPO3\\CMS\\', '', $target);
// Limit length if too long
if (strlen($target) > 80) {
return substr($target, 0, 77) . '...';
}
return $target;
}
}