239 lines
7.6 KiB
PHP
239 lines
7.6 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\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;
|
|
}
|
|
}
|