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 */ 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('%d 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; } }