warn('No routes found in manifest.'); return 0; } // Collect all routes in a flat array $all_routes = $this->collect_routes($routes); // Apply filters $filtered = $this->filter_routes($all_routes); if (empty($filtered)) { $this->warn('No routes match the specified filters.'); return 0; } // Output as JSON if requested if ($this->option('json')) { $this->line(json_encode($filtered, JSON_PRETTY_PRINT)); return 0; } // Display routes in greppable format foreach ($filtered as $route) { $this->display_route($route); } return 0; } /** * Collect all routes from manifest into flat array * * @param array $routes * @return array */ protected function collect_routes($routes) { $all_routes = []; foreach ($routes as $type => $type_routes) { foreach ($type_routes as $pattern => $methods) { foreach ($methods as $method => $handlers) { // Handle new structure where each method can have multiple handlers if (!isset($handlers[0])) { // Old structure - single handler $handlers = [$handlers]; } foreach ($handlers as $handler) { $all_routes[] = [ 'type' => $type, 'pattern' => $pattern, 'method' => $method, 'class' => $handler['class'] ?? 'N/A', 'action' => $handler['method'] ?? 'N/A', 'middleware' => $this->get_middleware($handler), 'cache' => $this->get_cache_ttl($handler), 'name' => $handler['name'] ?? null, 'attributes' => $handler['attributes'] ?? [], ]; } } } } // Custom sort: hierarchical path segments with special rules usort($all_routes, function ($a, $b) { return $this->compare_routes($a['pattern'], $b['pattern']); }); return $all_routes; } /** * Filter routes based on command options * * @param array $routes * @return array */ protected function filter_routes($routes) { $filtered = $routes; // Filter by type $type = $this->option('type'); if ($type) { $filtered = array_filter($filtered, function ($route) use ($type) { return strcasecmp($route['type'], $type) === 0; }); } // Filter by method $method = $this->option('method'); if ($method) { $filtered = array_filter($filtered, function ($route) use ($method) { return strcasecmp($route['method'], $method) === 0; }); } // Filter by path pattern $path = $this->option('path'); if ($path) { $pattern = str_replace('*', '.*', $path); $filtered = array_filter($filtered, function ($route) use ($pattern) { return preg_match('#' . $pattern . '#i', $route['pattern']); }); } // Filter by class $class = $this->option('class'); if ($class) { $filtered = array_filter($filtered, function ($route) use ($class) { return stripos($route['class'], $class) !== false; }); } return array_values($filtered); } /** * Compare two route patterns for hierarchical sorting * * @param string $a First route pattern * @param string $b Second route pattern * @return int */ protected function compare_routes($a, $b) { // Split paths into segments $segments_a = explode('/', trim($a, '/')); $segments_b = explode('/', trim($b, '/')); // Compare segment by segment $max = max(count($segments_a), count($segments_b)); for ($i = 0; $i < $max; $i++) { $seg_a = $segments_a[$i] ?? ''; $seg_b = $segments_b[$i] ?? ''; // If one path ends here (empty segment), it comes first if ($seg_a === '' && $seg_b !== '') { return -1; } if ($seg_b === '' && $seg_a !== '') { return 1; } // Both segments exist - check if they're parameters $is_param_a = str_starts_with($seg_a, ':'); $is_param_b = str_starts_with($seg_b, ':'); // Non-parameters come before parameters if (!$is_param_a && $is_param_b) { return -1; } if ($is_param_a && !$is_param_b) { return 1; } // Both same type (both params or both not), compare alphabetically $cmp = strcmp($seg_a, $seg_b); if ($cmp !== 0) { return $cmp; } } return 0; } /** * Display a single route in greppable key-value format * * @param array $route */ protected function display_route($route) { $parts = []; // Build key-value pairs $parts[] = 'path=' . $route['pattern']; $parts[] = 'method=' . $route['method']; $parts[] = 'controller=' . class_basename($route['class']); $parts[] = 'action=' . $route['action']; // Get file path if we can determine it if ($route['class'] !== 'N/A') { try { $file = Manifest::php_find_class(class_basename($route['class'])); if ($file) { $parts[] = 'file=' . $file; } } catch (Exception $e) { // Class not found in manifest, skip file path } } // Add optional fields if present if ($route['middleware']) { $parts[] = 'middleware=' . $route['middleware']; } if ($route['cache']) { $parts[] = 'cache=' . $route['cache'] . 's'; } if ($route['name']) { $parts[] = 'name=' . $route['name']; } $this->line(implode(' ', $parts)); } /** * Get middleware string from handler * * @param array $handler * @return string|null */ protected function get_middleware($handler) { $attributes = $handler['attributes'] ?? []; foreach ($attributes as $attr) { if (isset($attr['class']) && str_ends_with($attr['class'], 'Middleware')) { $middleware = $attr['args']['middleware'] ?? $attr['args'][0] ?? null; if (is_array($middleware)) { return implode(',', $middleware); } return $middleware; } } return null; } /** * Get cache TTL from handler * * @param array $handler * @return int|null */ protected function get_cache_ttl($handler) { $attributes = $handler['attributes'] ?? []; foreach ($attributes as $attr) { if (isset($attr['class']) && str_ends_with($attr['class'], 'Cache')) { return $attr['args']['ttl'] ?? $attr['args'][0] ?? null; } } return null; } }