From ce3d9f1aecf30665a15d932e176428c941b20513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Mu=CC=88ller?= Date: Sat, 12 Oct 2024 09:39:30 +0200 Subject: [PATCH 1/3] BUGFIX: Static compile attribute routes The necessary reflection data used to build the routes from attributes is not available at (Production) runtime. This is an issue in itself but not trivial to fix, therefore we fix this here by using the `CompileStatic` attribute to bring the necessary data over from compiletime. Fixes: #3400 --- .../Mvc/Routing/AttributeRoutesProvider.php | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php index df77ab0840..0f53ed76fd 100644 --- a/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php +++ b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php @@ -78,9 +78,7 @@ public function __construct( public function getRoutes(): Routes { $routes = []; - $annotatedClasses = $this->reflectionService->getClassesContainingMethodsAnnotatedWith(Flow\Route::class); - - foreach ($annotatedClasses as $className) { + foreach (static::compileRoutesConfiguration($this->objectManager) as $className => $routesForClass) { $includeClassName = false; foreach ($this->classNames as $classNamePattern) { if (fnmatch($classNamePattern, $className, FNM_NOESCAPE)) { @@ -92,12 +90,37 @@ public function getRoutes(): Routes continue; } + $routes = [...$routes, ...$routesForClass]; + } + + $routes = array_map(static fn(array $routeConfiguration): Route => Route::fromConfiguration($routeConfiguration), $routes); + return Routes::create(...$routes); + } + + /** + * @param ObjectManagerInterface $objectManager + * @return array + * @throws InvalidActionNameException + * @throws InvalidControllerException + * @throws \Neos\Flow\Utility\Exception + * @throws \Neos\Utility\Exception\FilesException + * @throws \ReflectionException + */ + #[Flow\CompileStatic] + public static function compileRoutesConfiguration(ObjectManagerInterface $objectManager): array + { + $reflectionService = $objectManager->get(ReflectionService::class); + + $routesByClassName = []; + $annotatedClasses = $reflectionService->getClassesContainingMethodsAnnotatedWith(Flow\Route::class); + + foreach ($annotatedClasses as $className) { if (!in_array(ActionController::class, class_parents($className), true)) { throw new InvalidControllerException('TODO: Currently #[Flow\Route] is only supported for ActionController. See https://github.com/neos/flow-development-collection/issues/3335.'); } - $controllerObjectName = $this->objectManager->getCaseSensitiveObjectName($className); - $controllerPackageKey = $this->objectManager->getPackageKeyByObjectName($controllerObjectName); + $controllerObjectName = $objectManager->getCaseSensitiveObjectName($className); + $controllerPackageKey = $objectManager->getPackageKeyByObjectName($controllerObjectName); $controllerPackageNamespace = str_replace('.', '\\', $controllerPackageKey); if (!str_ends_with($className, 'Controller')) { throw new InvalidControllerException('Only for controller classes'); @@ -109,17 +132,18 @@ public function getRoutes(): Routes $controllerName = substr($localClassName, 11); $subPackage = null; } elseif (str_contains($localClassName, '\\Controller\\')) { - list($subPackage, $controllerName) = explode('\\Controller\\', $localClassName); + [$subPackage, $controllerName] = explode('\\Controller\\', $localClassName); } else { throw new InvalidControllerException('Unknown controller pattern'); } - $annotatedMethods = $this->reflectionService->getMethodsAnnotatedWith($className, Flow\Route::class); + $routesByClassName[$className] = []; + $annotatedMethods = $reflectionService->getMethodsAnnotatedWith($className, Flow\Route::class); foreach ($annotatedMethods as $methodName) { if (!str_ends_with($methodName, 'Action')) { throw new InvalidActionNameException('Only for action methods'); } - $annotations = $this->reflectionService->getMethodAnnotations($className, $methodName, Flow\Route::class); + $annotations = $reflectionService->getMethodAnnotations($className, $methodName, Flow\Route::class); foreach ($annotations as $annotation) { if ($annotation instanceof Flow\Route) { $controller = substr($controllerName, 0, -10); @@ -139,11 +163,12 @@ public function getRoutes(): Routes $annotation->defaults ?? [] ) ]; - $routes[] = Route::fromConfiguration($configuration); + $routesByClassName[$className][] = $configuration; } } } } - return Routes::create(...$routes); + + return $routesByClassName; } } From 009c66313847dd7cb8bfe4b6f0a885d3d2dbda90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Mu=CC=88ller?= Date: Sat, 12 Oct 2024 10:00:10 +0200 Subject: [PATCH 2/3] Cleanup tests and codebase after bugfix --- .../Mvc/Routing/AttributeRoutesProvider.php | 3 +-- .../AttributeRoutesProviderFactory.php | 3 --- .../Routing/AttributeRoutesProviderTest.php | 20 +++++++++++++++++-- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php index 0f53ed76fd..0445c2a8da 100644 --- a/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php +++ b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php @@ -69,7 +69,6 @@ final class AttributeRoutesProvider implements RoutesProviderInterface * @param array $classNames */ public function __construct( - public readonly ReflectionService $reflectionService, public readonly ObjectManagerInterface $objectManager, public readonly array $classNames, ) { @@ -93,7 +92,7 @@ public function getRoutes(): Routes $routes = [...$routes, ...$routesForClass]; } - $routes = array_map(static fn(array $routeConfiguration): Route => Route::fromConfiguration($routeConfiguration), $routes); + $routes = array_map(static fn (array $routeConfiguration): Route => Route::fromConfiguration($routeConfiguration), $routes); return Routes::create(...$routes); } diff --git a/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProviderFactory.php b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProviderFactory.php index ff6aa1b81c..d9920bb0b8 100644 --- a/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProviderFactory.php +++ b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProviderFactory.php @@ -15,12 +15,10 @@ namespace Neos\Flow\Mvc\Routing; use Neos\Flow\ObjectManagement\ObjectManagerInterface; -use Neos\Flow\Reflection\ReflectionService; class AttributeRoutesProviderFactory implements RoutesProviderFactoryInterface { public function __construct( - public readonly ReflectionService $reflectionService, public readonly ObjectManagerInterface $objectManager, ) { } @@ -31,7 +29,6 @@ public function __construct( public function createRoutesProvider(array $options): RoutesProviderInterface { return new AttributeRoutesProvider( - $this->reflectionService, $this->objectManager, $options['classNames'] ?? [], ); diff --git a/Neos.Flow/Tests/Unit/Mvc/Routing/AttributeRoutesProviderTest.php b/Neos.Flow/Tests/Unit/Mvc/Routing/AttributeRoutesProviderTest.php index 4ee9cd3459..5daf788efc 100644 --- a/Neos.Flow/Tests/Unit/Mvc/Routing/AttributeRoutesProviderTest.php +++ b/Neos.Flow/Tests/Unit/Mvc/Routing/AttributeRoutesProviderTest.php @@ -35,8 +35,12 @@ public function setUp(): void $this->mockReflectionService = $this->createMock(ReflectionService::class); $this->mockObjectManager = $this->createMock(ObjectManagerInterface::class); + $this->mockObjectManager->expects(self::any()) + ->method('get') + ->with(ReflectionService::class) + ->willReturn($this->mockReflectionService); + $this->annotationRoutesProvider = new Routing\AttributeRoutesProvider( - $this->mockReflectionService, $this->mockObjectManager, ['Vendor\\Example\\Controller\\*'] ); @@ -136,10 +140,22 @@ class ExampleController extends \Neos\Flow\Mvc\Controller\ActionController { */ public function annotationsOutsideClassNamesAreIgnored(): void { + $controllerclassName = 'Neos\Flow\Mvc\Controller\StandardController'; + + $this->mockObjectManager->expects(self::once()) + ->method('getCaseSensitiveObjectName') + ->with($controllerclassName) + ->willReturn($controllerclassName); + + $this->mockObjectManager->expects(self::once()) + ->method('getPackageKeyByObjectName') + ->with($controllerclassName) + ->willReturn('Neos.Flow'); + $this->mockReflectionService->expects($this->once()) ->method('getClassesContainingMethodsAnnotatedWith') ->with(Flow\Route::class) - ->willReturn(['Vendor\Other\Controller\ExampleController']); + ->willReturn([$controllerclassName]); $this->assertEquals(Routes::empty(), $this->annotationRoutesProvider->getRoutes()); } From ab6c0a94ea55c46fe351e3c01928aca92f8c3401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Mu=CC=88ller?= Date: Sat, 12 Oct 2024 10:12:56 +0200 Subject: [PATCH 3/3] Help stan --- Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php index 0445c2a8da..039215da9c 100644 --- a/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php +++ b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php @@ -98,7 +98,7 @@ public function getRoutes(): Routes /** * @param ObjectManagerInterface $objectManager - * @return array + * @return array> * @throws InvalidActionNameException * @throws InvalidControllerException * @throws \Neos\Flow\Utility\Exception