From 9552f4c745bc56a576a02197b00326ef9d156f19 Mon Sep 17 00:00:00 2001 From: AmirHossein Date: Sat, 26 Jul 2025 04:26:11 +0330 Subject: [PATCH 1/7] feat: Add initial routing attributes --- src/Illuminate/Routing/Attributes/Any.php | 24 +++++++++++++++++ src/Illuminate/Routing/Attributes/Delete.php | 24 +++++++++++++++++ src/Illuminate/Routing/Attributes/Get.php | 24 +++++++++++++++++ src/Illuminate/Routing/Attributes/Group.php | 22 ++++++++++++++++ src/Illuminate/Routing/Attributes/Matches.php | 26 +++++++++++++++++++ src/Illuminate/Routing/Attributes/Options.php | 24 +++++++++++++++++ src/Illuminate/Routing/Attributes/Patch.php | 24 +++++++++++++++++ src/Illuminate/Routing/Attributes/Post.php | 24 +++++++++++++++++ src/Illuminate/Routing/Attributes/Put.php | 24 +++++++++++++++++ .../Routing/Attributes/RouteAttribute.php | 24 +++++++++++++++++ 10 files changed, 240 insertions(+) create mode 100644 src/Illuminate/Routing/Attributes/Any.php create mode 100644 src/Illuminate/Routing/Attributes/Delete.php create mode 100644 src/Illuminate/Routing/Attributes/Get.php create mode 100644 src/Illuminate/Routing/Attributes/Group.php create mode 100644 src/Illuminate/Routing/Attributes/Matches.php create mode 100644 src/Illuminate/Routing/Attributes/Options.php create mode 100644 src/Illuminate/Routing/Attributes/Patch.php create mode 100644 src/Illuminate/Routing/Attributes/Post.php create mode 100644 src/Illuminate/Routing/Attributes/Put.php create mode 100644 src/Illuminate/Routing/Attributes/RouteAttribute.php diff --git a/src/Illuminate/Routing/Attributes/Any.php b/src/Illuminate/Routing/Attributes/Any.php new file mode 100644 index 000000000000..dc662b2db1c7 --- /dev/null +++ b/src/Illuminate/Routing/Attributes/Any.php @@ -0,0 +1,24 @@ + Date: Sat, 26 Jul 2025 04:26:25 +0330 Subject: [PATCH 2/7] feat: Add AttributeRouteRegistrar and marker interface --- .../Routing/AttributeRouteController.php | 8 + .../Routing/AttributeRouteRegistrar.php | 180 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 src/Illuminate/Contracts/Routing/AttributeRouteController.php create mode 100644 src/Illuminate/Routing/AttributeRouteRegistrar.php diff --git a/src/Illuminate/Contracts/Routing/AttributeRouteController.php b/src/Illuminate/Contracts/Routing/AttributeRouteController.php new file mode 100644 index 000000000000..4d3f52d151f6 --- /dev/null +++ b/src/Illuminate/Contracts/Routing/AttributeRouteController.php @@ -0,0 +1,8 @@ +psr4Paths = $this->getPsr4Paths(); + } + + /** + * Scan the given directories and register any found attribute-based routes. + * + * @param string ...$controllerDirectories + * @return void + */ + public function register(...$controllerDirectories) + { + if (empty($controllerDirectories)) { + return; + } + + $finder = (new Finder)->files()->in($controllerDirectories)->name('*.php'); + + foreach ($finder as $file) { + $className = $this->getClassFromFile($file->getRealPath()); + + if ($className && class_exists($className) && is_a($className, AttributeRouteController::class, true)) { + $this->registerControllerRoutes($className); + } + } + } + + /** + * Registers all routes for a given controller class. + * + * @param string $controllerClassName The fully qualified class name of the controller. + * @return void + */ + public function registerControllerRoutes($controllerClassName) + { + $reflectionClass = new ReflectionClass($controllerClassName); + + $groupAttributes = $this->getGroupAttributes($reflectionClass) ?? []; + + $this->router->group($groupAttributes, function (Router $router) use ($reflectionClass) { + foreach ($reflectionClass->getMethods() as $method) { + $attributes = $method->getAttributes(RouteAttribute::class, \ReflectionAttribute::IS_INSTANCEOF); + + foreach ($attributes as $attribute) { + try { + $instance = $attribute->newInstance(); + $route = $router->addRoute( + $instance->methods, + $instance->path, + [$reflectionClass->getName(), $method->getName()] + ); + $this->applyRouteOptions($route, $instance); + } catch (\Throwable $e) { + report($e); + } + } + } + }); + } + + /** + * Applies all options from a RouteAttribute instance to a route. + * (MVP Version: Does not include complex options) + * + * @param \Illuminate\Routing\Route $route + * @param \Illuminate\Routing\Attributes\RouteAttribute $instance + * @return void + */ + protected function applyRouteOptions(Route $route, RouteAttribute $instance): void + { + if ($instance->name) $route->name($instance->name); + if ($instance->middleware) $route->middleware($instance->middleware); + if ($instance->where) $route->where($instance->where); + + // Mark the route for the route:list command + $route->setAction(array_merge($route->getAction(), ['is_attribute_route' => true])); + } + + /** + * Gets the properties from a single #[Group] attribute on a class. + * (MVP Version: Does not support repeatable groups) + * + * @param \ReflectionClass $reflectionClass + * @return array|null + */ + protected function getGroupAttributes(ReflectionClass $reflectionClass): ?array + { + $attributes = $reflectionClass->getAttributes(Group::class); + + if (count($attributes) === 0) { + return null; + } + + try { + /** @var Group $group */ + $group = $attributes[0]->newInstance(); + + return array_filter([ + 'prefix' => $group->prefix, + 'middleware' => $group->middleware, + 'as' => $group->name, + 'where' => $group->where, + ]); + } catch (\Throwable $e) { + report($e); + return null; + } + } + + /** + * Derive the fully qualified class name from a file path. + * + * This implementation uses the project's Composer PSR-4 map to determine + * the class name, making it compatible with any autoloaded directory. + * + * @param string $path + * @return string|null + */ + protected function getClassFromFile($path) + { + foreach ($this->psr4Paths as $namespace => $paths) { + foreach ((array) $paths as $psr4Path) { + if (Str::startsWith($path, $psr4Path)) { + $relativePath = Str::of($path) + ->after($psr4Path) + ->trim(DIRECTORY_SEPARATOR) + ->replace(['/', '.php'], ['\\', '']) + ->toString(); + + return $namespace . $relativePath; + } + } + } + + return null; + } + + /** + * Load the Composer PSR-4 autoloading map. + * + * This map is used to convert a file path into a fully qualified class name. + * + * @return array + */ + protected function getPsr4Paths() + { + $composerPath = $this->app->basePath('vendor/composer/autoload_psr4.php'); + + return file_exists($composerPath) ? require $composerPath : []; + } +} From 31a44f55989b833be7b4f466cfec407cfaa48800 Mon Sep 17 00:00:00 2001 From: AmirHossein Date: Sat, 26 Jul 2025 04:26:41 +0330 Subject: [PATCH 3/7] feat: Integrate attribute routing into the application bootstrap process --- .../Configuration/ApplicationBuilder.php | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index e0ffdd534495..8b82f51185ad 100644 --- a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -13,6 +13,8 @@ use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance; use Illuminate\Foundation\Support\Providers\EventServiceProvider as AppEventServiceProvider; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as AppRouteServiceProvider; +use Illuminate\Routing\Router; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Broadcast; use Illuminate\Support\Facades\Event; @@ -43,6 +45,13 @@ class ApplicationBuilder */ protected array $pageMiddleware = []; + /** + * The attribute routing configurations. + * + * @var array + */ + protected array $attributeRoutingConfigurations = []; + /** * Create a new application builder instance. */ @@ -202,7 +211,9 @@ protected function buildRoutingCallback(array|string|null $web, string $apiPrefix, ?callable $then) { - return function () use ($web, $api, $pages, $health, $apiPrefix, $then) { + return function (Router $router) use ($web, $api, $pages, $health, $apiPrefix, $then) { + $this->registerAttributeRoutes($router); + if (is_string($api) || is_array($api)) { if (is_array($api)) { foreach ($api as $apiRoute) { @@ -265,6 +276,66 @@ class_exists(Folio::class)) { }; } + /** + * Configure attribute-based routing. + * + * @param array|string|null $web + * @param array|string|null $api + * @return $this + */ + public function withAttributeRouting( + array|string|null $web = null, + array|string|null $api = null + ) { + $groups = []; + + if (is_null($web) && is_null($api)) { + $groups['web'] = [app_path('Http/Controllers')]; + } else { + if (! is_null($web)) { + $groups['web'] = Arr::wrap($web); + } + if (! is_null($api)) { + $groups['api'] = Arr::wrap($api); + } + } + + $this->attributeRoutingConfigurations = $groups; + + return $this; + } + + /** + * Register all the configured attribute-based routes. + * + * @param \Illuminate\Routing\Router $router + * @return void + */ + protected function registerAttributeRoutes(Router $router): void + { + if (empty($this->attributeRoutingConfigurations)) { + return; + } + + $registrar = $this->app->make(\Illuminate\Routing\AttributeRouteRegistrar::class); + + foreach ($this->attributeRoutingConfigurations as $groupName => $paths) { + if (empty($paths)) { + continue; + } + + $groupOptions = ['middleware' => $groupName]; + + if ($groupName === 'api') { + $groupOptions['prefix'] = 'api'; + } + + $router->group($groupOptions, function () use ($registrar, $paths) { + $registrar->register(...$paths); + }); + } + } + /** * Register the global middleware, middleware groups, and middleware aliases for the application. * From e41143eada10d8b99f97af72c8850b43188621f4 Mon Sep 17 00:00:00 2001 From: AmirHossein Date: Sat, 26 Jul 2025 04:26:52 +0330 Subject: [PATCH 4/7] test: Add initial feature tests for attribute routing --- tests/Routing/AttributeRoutingTest.php | 114 +++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 tests/Routing/AttributeRoutingTest.php diff --git a/tests/Routing/AttributeRoutingTest.php b/tests/Routing/AttributeRoutingTest.php new file mode 100644 index 000000000000..be44ff1d9235 --- /dev/null +++ b/tests/Routing/AttributeRoutingTest.php @@ -0,0 +1,114 @@ +container = Container::setInstance(new Container); + + $this->router = new Router(new Dispatcher, $this->container); + $this->container->instance('router', $this->router); + + Facade::setFacadeApplication($this->container); + + $request = Request::create('http://example.com'); + $this->container->instance('url', new UrlGenerator( + $this->router->getRoutes(), $request + )); + + $appMock = m::mock(Application::class); + $appMock->shouldReceive('basePath')->andReturn(''); + $this->container->instance(Application::class, $appMock); + + $this->registrar = new AttributeRouteRegistrar($appMock, $this->router); + + $this->registrar->registerControllerRoutes(BasicController::class); + $this->registrar->registerControllerRoutes(GroupController::class); + + $this->router->getRoutes()->refreshNameLookups(); + } + + protected function tearDown(): void + { + m::close(); + Facade::clearResolvedInstances(); + Container::setInstance(null); + parent::tearDown(); + } + + public function test_it_registers_and_accesses_a_basic_get_route(): void + { + $request = Request::create('/get', 'GET'); + $response = $this->router->dispatch($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('get success', $response->getContent()); + } + + public function test_it_registers_a_basic_post_route(): void + { + $request = Request::create('/post', 'POST'); + $response = $this->router->dispatch($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('post success', $response->getContent()); + } + + public function test_it_applies_a_name_to_a_route(): void + { + $this->assertTrue(Route::has('get')); + $this->assertEquals('http://example.com/get', route('get')); + } + + public function test_it_applies_group_prefix(): void + { + $request = Request::create('/group/route', 'GET'); + $response = $this->router->dispatch($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('grouped route', $response->getContent()); + } + + public function test_it_applies_group_name_prefix(): void + { + $this->assertTrue(Route::has('group.route')); + $this->assertEquals('http://example.com/group/route', route('group.route')); + } +} From 02396fcac1b9d21f8ab59619203def73fe4cedd2 Mon Sep 17 00:00:00 2001 From: AmirHossein Date: Sat, 26 Jul 2025 04:53:45 +0330 Subject: [PATCH 5/7] fix: style --- .../Routing/AttributeRouteRegistrar.php | 19 ++++++++++++------- src/Illuminate/Routing/Attributes/Group.php | 3 ++- .../Routing/Attributes/RouteAttribute.php | 3 ++- tests/Routing/AttributeRoutingTest.php | 14 ++++++++++---- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Routing/AttributeRouteRegistrar.php b/src/Illuminate/Routing/AttributeRouteRegistrar.php index fae13b8d0484..852ace85d114 100644 --- a/src/Illuminate/Routing/AttributeRouteRegistrar.php +++ b/src/Illuminate/Routing/AttributeRouteRegistrar.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Routing\AttributeRouteController; use Illuminate\Routing\Attributes\Group; +use Illuminate\Routing\Attributes\Patch; use Illuminate\Routing\Attributes\RouteAttribute; use Illuminate\Support\Str; use ReflectionClass; @@ -57,7 +58,7 @@ public function register(...$controllerDirectories) /** * Registers all routes for a given controller class. * - * @param string $controllerClassName The fully qualified class name of the controller. + * @param string $controllerClassName * @return void */ public function registerControllerRoutes($controllerClassName) @@ -89,7 +90,6 @@ public function registerControllerRoutes($controllerClassName) /** * Applies all options from a RouteAttribute instance to a route. - * (MVP Version: Does not include complex options) * * @param \Illuminate\Routing\Route $route * @param \Illuminate\Routing\Attributes\RouteAttribute $instance @@ -97,9 +97,15 @@ public function registerControllerRoutes($controllerClassName) */ protected function applyRouteOptions(Route $route, RouteAttribute $instance): void { - if ($instance->name) $route->name($instance->name); - if ($instance->middleware) $route->middleware($instance->middleware); - if ($instance->where) $route->where($instance->where); + if ($instance->name) { + $route->name($instance->name); + } + if ($instance->middleware) { + $route->middleware($instance->middleware); + } + if ($instance->where) { + $route->where($instance->where); + } // Mark the route for the route:list command $route->setAction(array_merge($route->getAction(), ['is_attribute_route' => true])); @@ -107,7 +113,6 @@ protected function applyRouteOptions(Route $route, RouteAttribute $instance): vo /** * Gets the properties from a single #[Group] attribute on a class. - * (MVP Version: Does not support repeatable groups) * * @param \ReflectionClass $reflectionClass * @return array|null @@ -156,7 +161,7 @@ protected function getClassFromFile($path) ->replace(['/', '.php'], ['\\', '']) ->toString(); - return $namespace . $relativePath; + return $namespace.$relativePath; } } } diff --git a/src/Illuminate/Routing/Attributes/Group.php b/src/Illuminate/Routing/Attributes/Group.php index 98b076a28b3f..331de50120a8 100644 --- a/src/Illuminate/Routing/Attributes/Group.php +++ b/src/Illuminate/Routing/Attributes/Group.php @@ -18,5 +18,6 @@ public function __construct( public ?string $name = null, public array|string $middleware = [], public array $where = [] - ) {} + ) { + } } diff --git a/src/Illuminate/Routing/Attributes/RouteAttribute.php b/src/Illuminate/Routing/Attributes/RouteAttribute.php index 1e2984b88105..43a0e9d974ec 100644 --- a/src/Illuminate/Routing/Attributes/RouteAttribute.php +++ b/src/Illuminate/Routing/Attributes/RouteAttribute.php @@ -20,5 +20,6 @@ public function __construct( public ?string $name = null, public array|string $middleware = [], public array $where = [] - ) { } + ) { + } } diff --git a/tests/Routing/AttributeRoutingTest.php b/tests/Routing/AttributeRoutingTest.php index be44ff1d9235..64727a30d2b9 100644 --- a/tests/Routing/AttributeRoutingTest.php +++ b/tests/Routing/AttributeRoutingTest.php @@ -7,12 +7,12 @@ use Illuminate\Contracts\Routing\AttributeRouteController; use Illuminate\Events\Dispatcher; use Illuminate\Http\Request; +use Illuminate\Routing\AttributeRouteRegistrar; use Illuminate\Routing\Attributes\Get; use Illuminate\Routing\Attributes\Group; use Illuminate\Routing\Attributes\Post; use Illuminate\Routing\Router; use Illuminate\Routing\UrlGenerator; -use Illuminate\Routing\AttributeRouteRegistrar; use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Route; use Mockery as m; @@ -22,15 +22,21 @@ class BasicController implements AttributeRouteController { #[Get('/get', name: 'get')] - public function get() { return 'get success'; } + public function get() { + return 'get success'; + } #[Post('/post')] - public function post() { return 'post success'; } + public function post() { + return 'post success'; + } } #[Group(prefix: 'group', name: 'group.')] class GroupController implements AttributeRouteController { #[Get('/route', name: 'route')] - public function route() { return 'grouped route'; } + public function route() { + return 'grouped route'; + } } // --- Test Class --- From 536a63dad76b6e86e5658b96b0e11e1cd57842d1 Mon Sep 17 00:00:00 2001 From: AmirHossein Date: Sat, 26 Jul 2025 04:55:52 +0330 Subject: [PATCH 6/7] fix: style --- src/Illuminate/Routing/AttributeRouteRegistrar.php | 2 +- tests/Routing/AttributeRoutingTest.php | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Routing/AttributeRouteRegistrar.php b/src/Illuminate/Routing/AttributeRouteRegistrar.php index 852ace85d114..8878c7f23253 100644 --- a/src/Illuminate/Routing/AttributeRouteRegistrar.php +++ b/src/Illuminate/Routing/AttributeRouteRegistrar.php @@ -5,7 +5,6 @@ use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Routing\AttributeRouteController; use Illuminate\Routing\Attributes\Group; -use Illuminate\Routing\Attributes\Patch; use Illuminate\Routing\Attributes\RouteAttribute; use Illuminate\Support\Str; use ReflectionClass; @@ -137,6 +136,7 @@ protected function getGroupAttributes(ReflectionClass $reflectionClass): ?array ]); } catch (\Throwable $e) { report($e); + return null; } } diff --git a/tests/Routing/AttributeRoutingTest.php b/tests/Routing/AttributeRoutingTest.php index 64727a30d2b9..e10f91a1c7c2 100644 --- a/tests/Routing/AttributeRoutingTest.php +++ b/tests/Routing/AttributeRoutingTest.php @@ -22,11 +22,13 @@ class BasicController implements AttributeRouteController { #[Get('/get', name: 'get')] - public function get() { + public function get() + { return 'get success'; } #[Post('/post')] - public function post() { + public function post() + { return 'post success'; } } @@ -34,7 +36,8 @@ public function post() { class GroupController implements AttributeRouteController { #[Get('/route', name: 'route')] - public function route() { + public function route() + { return 'grouped route'; } } From 527fdea41c7d1d5a82cc6fdfa46ab99072294fdf Mon Sep 17 00:00:00 2001 From: AmirHossein Date: Sat, 26 Jul 2025 04:57:15 +0330 Subject: [PATCH 7/7] fix: damn styleci --- tests/Routing/AttributeRoutingTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Routing/AttributeRoutingTest.php b/tests/Routing/AttributeRoutingTest.php index e10f91a1c7c2..abb3838f8c31 100644 --- a/tests/Routing/AttributeRoutingTest.php +++ b/tests/Routing/AttributeRoutingTest.php @@ -26,6 +26,7 @@ public function get() { return 'get success'; } + #[Post('/post')] public function post() {