diff --git a/doc/fields.rst b/doc/fields.rst index 26cdd0d3b5..f59fae39b0 100644 --- a/doc/fields.rst +++ b/doc/fields.rst @@ -1127,8 +1127,8 @@ the full address is only visible on the detail and edit pages:: [$local, $domain] = explode('@', $email, 2); $field->setFormattedValue(substr($local, 0, 1).'***@'.$domain); - } - } + } + } .. tip:: diff --git a/src/Router/AdminRouteGenerator.php b/src/Router/AdminRouteGenerator.php index 89bb5e2f4c..c1f3363106 100644 --- a/src/Router/AdminRouteGenerator.php +++ b/src/Router/AdminRouteGenerator.php @@ -251,7 +251,12 @@ private function generateAdminRoutes(): array EA::CRUD_ACTION => $actionRouteConfig['actionName'], ]; - $adminRoute = new Route($adminRoutePath, defaults: $defaults, methods: $actionRouteConfig['methods']); + $adminRoute = new Route($adminRoutePath); + // the framework-computed methods are applied first; if the user defined + // an explicit "methods" key in the #[AdminRoute] options, applyAdminRouteOptions() + // will overwrite them below + $adminRoute->setMethods($actionRouteConfig['methods']); + self::applyAdminRouteOptions($adminRoute, $actionRouteConfig['adminRouteOptions'] ?? [], $defaults); $adminRoutes[$adminRouteName] = $adminRoute; $addedRouteNames[] = $adminRouteName; } @@ -440,49 +445,65 @@ private function createRouteForAdminAttribute(AdminRoute $adminRouteAttribute, s { $route = new Route($routePath); - $routeOptions = $adminRouteAttribute->options; + $defaults = [ + '_controller' => $controllerFqcn.'::'.$methodName, + EA::ROUTE_CREATED_BY_EASYADMIN => true, + EA::DASHBOARD_CONTROLLER_FQCN => $dashboardFqcn, + EA::CRUD_CONTROLLER_FQCN => $controllerFqcn, + EA::CRUD_ACTION => $methodName, + ]; - if (isset($routeOptions['requirements'])) { - $route->setRequirements($routeOptions['requirements']); + self::applyAdminRouteOptions($route, $adminRouteAttribute->options, $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 static function applyAdminRouteOptions(Route $route, array $options, array $defaults): void + { + if (isset($options['requirements'])) { + $route->setRequirements($options['requirements']); } - if (isset($routeOptions['host'])) { - $route->setHost($routeOptions['host']); + if (isset($options['host'])) { + $route->setHost($options['host']); } - if (isset($routeOptions['methods'])) { - $route->setMethods($routeOptions['methods']); + if (isset($options['methods'])) { + $route->setMethods($options['methods']); } - if (isset($routeOptions['schemes'])) { - $route->setSchemes($routeOptions['schemes']); + 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); - - if (isset($routeOptions['utf8'])) { - $routeOptions['options']['utf8'] = $routeOptions['utf8']; + $route->setDefaults($mergedDefaults); + + $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; } /** @@ -687,6 +708,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; } } @@ -701,45 +726,15 @@ private function createDashboardRoute(string $routePath, array $routeOptions, st { $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 = [ + '_controller' => $dashboardFqcn.'::index', + EA::ROUTE_CREATED_BY_EASYADMIN => true, + EA::DASHBOARD_CONTROLLER_FQCN => $dashboardFqcn, + EA::CRUD_CONTROLLER_FQCN => null, + EA::CRUD_ACTION => null, + ]; - $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); - - if (isset($routeOptions['utf8'])) { - $routeOptions['options']['utf8'] = $routeOptions['utf8']; - } - if (isset($routeOptions['options'])) { - $route->setOptions($routeOptions['options']); - } + self::applyAdminRouteOptions($route, $routeOptions, $defaults); return $route; } diff --git a/tests/Functional/AdminRoute/AdminRouteTest.php b/tests/Functional/AdminRoute/AdminRouteTest.php index 3a4b502977..826c27f56b 100644 --- a/tests/Functional/AdminRoute/AdminRouteTest.php +++ b/tests/Functional/AdminRoute/AdminRouteTest.php @@ -165,6 +165,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'); + } }