plugin icon indicating copy to clipboard operation
plugin copied to clipboard

[Feature Request]: Analyze cached routes

Open siamak2 opened this issue 3 months ago • 3 comments

Feature Description

I have a situation like #1126 where I define some routes in a macro. Since it's complex to analyze routes defined in a macro, maybe analyze the cached route file: bootstrap/cache/route-v7.php. This file contains all the routes defined in macro and I can setup a file watcher to run artisan route:cache after route files modified.

siamak2 avatar Oct 01 '25 17:10 siamak2

Could you show the macro? We already have some solution for this case.

adelf avatar Oct 01 '25 17:10 adelf

Mine is complex. it may look like resource route but it has some differences.

Route::macro( 'myEditor', function ($name, $controller, $options = []) {
	Route::name( $name . '.' )->prefix( $name )->controller( $controller )->group( function () use ($name, $options) {
		$variableName = $options['variableName'] ?? Str::singular( $name );
		Route::get( '/', 'index' )->name( 'index' );
		Route::post( '/', 'store' )->name( 'store' );
		Route::post( '/delete', 'destroy' )->name( 'destroy' );
		Route::match( ['put', 'patch'], '/{' . $variableName . '}', 'update' )->name( 'update' );
	} );
} );

siamak2 avatar Oct 01 '25 17:10 siamak2

Right now as a workaround i created a command to find routes created by macros and put in a helper file for laravel-idea to pick them up. This method needs two classes:

MacroRouter class

place the following code in app/Classes/MacroRouter.php:

<?php

namespace App\Classes;

use Illuminate\Routing\Route;
use Illuminate\Routing\Router;

class MacroRouter extends Router
{
	/**
	 * @var array|Route[][]
	 */
	protected(set) array $macroRouteDefinitions = [];
	
	public function __call($method, $parameters) {
		if ( ! static::hasMacro( $method ) ) {
			return parent::__call( $method, $parameters );
		}
		$before = $this->getRoutes()->getRoutes();
		$result = $this->macroCall( $method, $parameters );
		$after = $this->getRoutes()->getRoutes();
		$new = array_diff( array_keys( $after ), array_keys( $before ) );
		if ( ! array_key_exists( $method, $this->macroRouteDefinitions ) ) {
			$this->macroRouteDefinitions[$method] = [];
		}
		foreach ($new as $item) {
			$this->macroRouteDefinitions[$method][] = $after[$item];
		}
		return $result;
	}
}

AutoCompleteForMacroRoutesCommand class

place the following code to commands directory:

<?php

namespace App\Console\Commands;

use Closure;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;

class AutoCompleteForMacroRoutesCommand extends Command
{
	/**
	 * The name and signature of the console command.
	 *
	 * @var string
	 */
	protected $signature = 'route:autocomplete';
	
	/**
	 * The console command description.
	 *
	 * @var string
	 */
	protected $description = 'Command description';
	
	/**
	 * Execute the console command.
	 */
	public function handle(Filesystem $files) {
		$macroRoutes = app( 'router' )->macroRouteDefinitions;
		$file = '<?php' . PHP_EOL;
		foreach ($macroRoutes as $macro => $routes) {
			$file .= "\n// Created by '$macro' macro.\n";
			foreach ($routes as $route) {
				$methods = array_diff( $route->methods(), ['HEAD'] );
				$method = count( $methods ) === 1 ? strtolower( reset( $methods ) ) : 'match';
				$file .= "Route::$method(";
				if ( $method === 'match' ) {
					$file .= "['" . implode( "', '", $methods ) . "'], ";
				}
				$file .= "'" . $route->uri() . "', ";
				$action = $route->getAction();
				if ( isset( $action['uses'] ) && $action['uses'] instanceof Closure ) {
					// For Closures, we can only represent them as a generic closure stub
					$file .= 'function () { /* ... */ })';
				}
				elseif ( is_string( $action['uses'] ) ) {
					// Controller@method or named string handler
					$file .= "'{$action['uses']}')";
				}
				elseif ( is_array( $action['uses'] ) && isset( $action['uses'][0] ) && isset( $action['uses'][1] ) ) {
					// [Controller::class, 'method'] format
					$controller = is_object( $action['uses'][0] ) ? get_class( $action['uses'][0] ) : $action['uses'][0];
					$file .= "['$controller', '{$action['uses'][1]}'])";
				}
				else {
					// Fallback for unexpected actions
					$file .= '/* UNKNOWN ACTION */)';
				}
				// Name
				if ( $name = $route->getName() ) {
					$file .= "->name('$name')";
				}
				
				// Middleware
				if ( $middleware = $route->gatherMiddleware() ) {
					$file .= "->middleware(['" . implode( "', '", $middleware ) . "'])";
				}
				
				// Where (Parameter constraints)
				if ( $wheres = $route->wheres ) {
					foreach ($wheres as $key => $value) {
						$file .= "->where('$key', '$value')";
					}
				}
				$file .= ";\n";
			}
		}
		
		$helperPath = base_path( '_ide_helper' );
		if ( ! $files->isDirectory( $helperPath ) ) {
			$files->makeDirectory( $helperPath );
		}
		$helperPath .= '/macro_routes.php';
		
		$files->put( $helperPath, $file );
		$this->line( "File created/updated at: $helperPath:1" );
	}
}

After that, this should be added to RouteServiceProvider's boot method:

if ( app()->runningConsoleCommand( 'route:autocomplete' ) ) {
			$this->app->bind(
				Router::class,
				MacroRouter::class
			);
			$this->app->singleton( 'router', function ($app) {
				return new MacroRouter( $app['events'], $app );
			} );
			Route::clearResolvedInstance( 'router' );
		}

Then setup file watcher on routes directory to run artisan route:autocomplete.

siamak2 avatar Oct 16 '25 21:10 siamak2