From 80916b1dd71382f829429c960a5088e0e41d6108 Mon Sep 17 00:00:00 2001 From: lacatoire Date: Mon, 20 Apr 2026 17:24:44 +0200 Subject: [PATCH 1/3] Apply AdminRoute options on CRUD controller action routes --- src/Router/AdminRouteGenerator.php | 128 +++++++++--------- .../Functional/AdminRoute/AdminRouteTest.php | 37 +++++ .../StandaloneMethodsCrudController.php | 27 ++++ 3 files changed, 127 insertions(+), 65 deletions(-) diff --git a/src/Router/AdminRouteGenerator.php b/src/Router/AdminRouteGenerator.php index 88f9ce54d6..45c3f8adc4 100644 --- a/src/Router/AdminRouteGenerator.php +++ b/src/Router/AdminRouteGenerator.php @@ -235,7 +235,9 @@ private function generateAdminRoutes(): array EA::CRUD_ACTION => $actionRouteConfig['actionName'], ]; - $adminRoute = new Route($adminRoutePath, defaults: $defaults, methods: $actionRouteConfig['methods']); + $adminRoute = new Route($adminRoutePath); + $adminRoute->setMethods($actionRouteConfig['methods']); + $this->applyAdminRouteOptions($adminRoute, $actionRouteConfig['adminRouteOptions'] ?? [], $defaults); $adminRoutes[$adminRouteName] = $adminRoute; $addedRouteNames[] = $adminRouteName; } @@ -423,50 +425,68 @@ private function generateAdminRoutes(): array private function createRouteForAdminAttribute(AdminRoute $adminRouteAttribute, string $routePath, string $dashboardFqcn, string $controllerFqcn, string $methodName): Route { $route = new Route($routePath); - $routeOptions = $adminRouteAttribute->options; - if (isset($routeOptions['requirements'])) { - $route->setRequirements($routeOptions['requirements']); - } - if (isset($routeOptions['host'])) { - $route->setHost($routeOptions['host']); - } if (isset($routeOptions['methods'])) { $route->setMethods($routeOptions['methods']); } - if (isset($routeOptions['schemes'])) { - $route->setSchemes($routeOptions['schemes']); + + $defaults = [ + '_controller' => $controllerFqcn.'::'.$methodName, + EA::ROUTE_CREATED_BY_EASYADMIN => true, + EA::DASHBOARD_CONTROLLER_FQCN => $dashboardFqcn, + EA::CRUD_CONTROLLER_FQCN => $controllerFqcn, + EA::CRUD_ACTION => $methodName, + ]; + + $this->applyAdminRouteOptions($route, $routeOptions, $defaults); + + return $route; + } + + /** + * Applies the options passed to an #[AdminRoute] attribute on a Symfony Route. + * + * The given $defaults are merged on top of any "defaults" defined in $options, + * so the framework-managed defaults (such as the controller FQCN) always win. + * + * @param array $options + * @param array $defaults + */ + private function applyAdminRouteOptions(Route $route, array $options, array $defaults): void + { + if (isset($options['requirements'])) { + $route->setRequirements($options['requirements']); + } + if (isset($options['host'])) { + $route->setHost($options['host']); + } + if (isset($options['schemes'])) { + $route->setSchemes($options['schemes']); } - if (isset($routeOptions['condition'])) { - $route->setCondition($routeOptions['condition']); + if (isset($options['condition'])) { + $route->setCondition($options['condition']); } - $defaults = $routeOptions['defaults'] ?? []; - if (isset($routeOptions['locale'])) { - $defaults['_locale'] = $routeOptions['locale']; + $mergedDefaults = array_merge($options['defaults'] ?? [], $defaults); + if (isset($options['locale'])) { + $mergedDefaults['_locale'] = $options['locale']; } - if (isset($routeOptions['format'])) { - $defaults['_format'] = $routeOptions['format']; + if (isset($options['format'])) { + $mergedDefaults['_format'] = $options['format']; } - if (isset($routeOptions['stateless'])) { - $defaults['_stateless'] = $routeOptions['stateless']; + if (isset($options['stateless'])) { + $mergedDefaults['_stateless'] = $options['stateless']; } - $defaults['_controller'] = $controllerFqcn.'::'.$methodName; - $defaults[EA::ROUTE_CREATED_BY_EASYADMIN] = true; - $defaults[EA::DASHBOARD_CONTROLLER_FQCN] = $dashboardFqcn; - $defaults[EA::CRUD_CONTROLLER_FQCN] = $controllerFqcn; - $defaults[EA::CRUD_ACTION] = $methodName; - $route->setDefaults($defaults); + $route->setDefaults($mergedDefaults); - if (isset($routeOptions['utf8'])) { - $routeOptions['options']['utf8'] = $routeOptions['utf8']; + $nativeRouteOptions = $options['options'] ?? []; + if (isset($options['utf8'])) { + $nativeRouteOptions['utf8'] = $options['utf8']; } - if (isset($routeOptions['options'])) { - $route->setOptions($routeOptions['options']); + if ([] !== $nativeRouteOptions) { + $route->setOptions($nativeRouteOptions); } - - return $route; } /** @@ -744,6 +764,10 @@ private function getCustomActionsConfig(string $crudControllerFqcn): array // store the actual action name for the route generation $customActionsConfig[$routeId]['actionName'] = $action; + + // keep the full set of options so they can be applied to the generated + // Symfony Route later (requirements, host, schemes, condition, etc.) + $customActionsConfig[$routeId]['adminRouteOptions'] = $adminRouteInstance->options; } continue; // used to skip checking for deprecated AdminAction @@ -831,45 +855,19 @@ private function createDashboardRoute(string $dashboardFqcn): ?array $route = new Route($routePath); - if (isset($routeOptions['requirements'])) { - $route->setRequirements($routeOptions['requirements']); - } - if (isset($routeOptions['host'])) { - $route->setHost($routeOptions['host']); - } if (isset($routeOptions['methods'])) { $route->setMethods($routeOptions['methods']); } - if (isset($routeOptions['schemes'])) { - $route->setSchemes($routeOptions['schemes']); - } - if (isset($routeOptions['condition'])) { - $route->setCondition($routeOptions['condition']); - } - $defaults = $routeOptions['defaults'] ?? []; - if (isset($routeOptions['locale'])) { - $defaults['_locale'] = $routeOptions['locale']; - } - if (isset($routeOptions['format'])) { - $defaults['_format'] = $routeOptions['format']; - } - if (isset($routeOptions['stateless'])) { - $defaults['_stateless'] = $routeOptions['stateless']; - } - $defaults['_controller'] = $dashboardFqcn.'::index'; - $defaults[EA::ROUTE_CREATED_BY_EASYADMIN] = true; - $defaults[EA::DASHBOARD_CONTROLLER_FQCN] = $dashboardFqcn; - $defaults[EA::CRUD_CONTROLLER_FQCN] = null; - $defaults[EA::CRUD_ACTION] = null; - $route->setDefaults($defaults); + $defaults = [ + '_controller' => $dashboardFqcn.'::index', + EA::ROUTE_CREATED_BY_EASYADMIN => true, + EA::DASHBOARD_CONTROLLER_FQCN => $dashboardFqcn, + EA::CRUD_CONTROLLER_FQCN => null, + EA::CRUD_ACTION => null, + ]; - if (isset($routeOptions['utf8'])) { - $routeOptions['options']['utf8'] = $routeOptions['utf8']; - } - if (isset($routeOptions['options'])) { - $route->setOptions($routeOptions['options']); - } + $this->applyAdminRouteOptions($route, $routeOptions, $defaults); return ['routeName' => $routeName, 'route' => $route]; } diff --git a/tests/Functional/AdminRoute/AdminRouteTest.php b/tests/Functional/AdminRoute/AdminRouteTest.php index b1cfb9e6d7..dcfa606cea 100644 --- a/tests/Functional/AdminRoute/AdminRouteTest.php +++ b/tests/Functional/AdminRoute/AdminRouteTest.php @@ -184,6 +184,43 @@ public function testStandaloneMethodCrudRoutes(): void $this->assertNull($router->getRouteCollection()->get('admin_standalone_methods')); } + public function testAdminRouteAdvancedOptionsOnCrudAction(): void + { + $client = static::createClient(); + $router = $client->getContainer()->get('router'); + $route = $router->getRouteCollection()->get('admin_standalone_methods_crud_action3'); + + $this->assertNotNull($route, 'Route "admin_standalone_methods_crud_action3" should exist'); + $this->assertSame('/admin/standalone-methods/crud/action3/{entityId}', $route->getPath()); + $this->assertSame(['entityId' => '\d+'], $route->getRequirements()); + $this->assertSame('admin.example.com', $route->getHost()); + $this->assertSame(['https'], $route->getSchemes()); + $this->assertSame('context.getMethod() in ["GET", "HEAD"]', $route->getCondition()); + $this->assertSame('Symfony\Component\Routing\RouteCompiler', $route->getOption('compiler_class')); + $this->assertTrue($route->getOption('utf8')); + // custom CRUD actions that don't declare `methods` in their options default to GET and POST + $this->assertSame(['GET', 'POST'], $route->getMethods()); + + $defaults = $route->getDefaults(); + $this->assertSame('bar', $defaults['foo']); + $this->assertSame('en', $defaults['_locale']); + $this->assertSame('html', $defaults['_format']); + $this->assertTrue($defaults['_stateless']); + $this->assertTrue($defaults[EA::ROUTE_CREATED_BY_EASYADMIN]); + } + + public function testAdminRouteRequirementsReturn404OnCrudAction(): void + { + $client = static::createClient(); + + // non-matching path (violates the requirement entityId=\d+) must return 404 + $client->request('GET', '/admin/standalone-methods/crud/action3/foo', [], [], [ + 'HTTPS' => 'on', + 'HTTP_HOST' => 'admin.example.com', + ]); + $this->assertResponseStatusCodeSame(404); + } + public function testRouteAccessibility(): void { $client = static::createClient(); diff --git a/tests/Functional/Apps/AdminRouteApp/src/Controller/StandaloneMethodsCrudController.php b/tests/Functional/Apps/AdminRouteApp/src/Controller/StandaloneMethodsCrudController.php index 923173a256..d33619fd41 100644 --- a/tests/Functional/Apps/AdminRouteApp/src/Controller/StandaloneMethodsCrudController.php +++ b/tests/Functional/Apps/AdminRouteApp/src/Controller/StandaloneMethodsCrudController.php @@ -35,4 +35,31 @@ public function action2(): Response { return new Response('Standalone CRUD Action 2'); } + + #[AdminRoute( + path: '/crud/action3/{entityId}', + name: 'crud_action3', + options: [ + 'requirements' => [ + 'entityId' => '\d+', + ], + 'options' => [ + 'compiler_class' => 'Symfony\Component\Routing\RouteCompiler', + ], + 'defaults' => [ + 'foo' => 'bar', + ], + 'host' => 'admin.example.com', + 'schemes' => 'https', + 'condition' => 'context.getMethod() in ["GET", "HEAD"]', + 'locale' => 'en', + 'format' => 'html', + 'utf8' => true, + 'stateless' => true, + ] + )] + public function action3(): Response + { + return new Response('Standalone CRUD Action 3'); + } } From 567b9751eddfde85ac0a223cabfa781998f99eeb Mon Sep 17 00:00:00 2001 From: lacatoire Date: Wed, 22 Apr 2026 23:15:18 +0200 Subject: [PATCH 2/3] clarify methods precedence comment in generateAdminRoutes() --- src/Router/AdminRouteGenerator.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Router/AdminRouteGenerator.php b/src/Router/AdminRouteGenerator.php index 45c3f8adc4..14d9f8d386 100644 --- a/src/Router/AdminRouteGenerator.php +++ b/src/Router/AdminRouteGenerator.php @@ -236,6 +236,9 @@ private function generateAdminRoutes(): array ]; $adminRoute = new Route($adminRoutePath); + // the framework-computed methods are applied first; if the user defined + // an explicit "methods" key in the #[AdminRoute] options, it takes + // precedence and overwrites them inside applyAdminRouteOptions() $adminRoute->setMethods($actionRouteConfig['methods']); $this->applyAdminRouteOptions($adminRoute, $actionRouteConfig['adminRouteOptions'] ?? [], $defaults); $adminRoutes[$adminRouteName] = $adminRoute; From 51c1108d576ceba41febbe33ac402f12adfb306f Mon Sep 17 00:00:00 2001 From: lacatoire Date: Thu, 23 Apr 2026 09:38:45 +0200 Subject: [PATCH 3/3] make applyAdminRouteOptions() static since it uses no instance state --- src/Router/AdminRouteGenerator.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Router/AdminRouteGenerator.php b/src/Router/AdminRouteGenerator.php index 14d9f8d386..fb9fb589c2 100644 --- a/src/Router/AdminRouteGenerator.php +++ b/src/Router/AdminRouteGenerator.php @@ -240,7 +240,7 @@ private function generateAdminRoutes(): array // an explicit "methods" key in the #[AdminRoute] options, it takes // precedence and overwrites them inside applyAdminRouteOptions() $adminRoute->setMethods($actionRouteConfig['methods']); - $this->applyAdminRouteOptions($adminRoute, $actionRouteConfig['adminRouteOptions'] ?? [], $defaults); + self::applyAdminRouteOptions($adminRoute, $actionRouteConfig['adminRouteOptions'] ?? [], $defaults); $adminRoutes[$adminRouteName] = $adminRoute; $addedRouteNames[] = $adminRouteName; } @@ -442,7 +442,7 @@ private function createRouteForAdminAttribute(AdminRoute $adminRouteAttribute, s EA::CRUD_ACTION => $methodName, ]; - $this->applyAdminRouteOptions($route, $routeOptions, $defaults); + self::applyAdminRouteOptions($route, $routeOptions, $defaults); return $route; } @@ -456,7 +456,7 @@ private function createRouteForAdminAttribute(AdminRoute $adminRouteAttribute, s * @param array $options * @param array $defaults */ - private function applyAdminRouteOptions(Route $route, array $options, array $defaults): void + private static function applyAdminRouteOptions(Route $route, array $options, array $defaults): void { if (isset($options['requirements'])) { $route->setRequirements($options['requirements']); @@ -870,7 +870,7 @@ private function createDashboardRoute(string $dashboardFqcn): ?array EA::CRUD_ACTION => null, ]; - $this->applyAdminRouteOptions($route, $routeOptions, $defaults); + self::applyAdminRouteOptions($route, $routeOptions, $defaults); return ['routeName' => $routeName, 'route' => $route]; }