diff --git a/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php index df77ab0840..039215da9c 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, ) { @@ -78,9 +77,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 +89,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 +131,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 +162,12 @@ public function getRoutes(): Routes $annotation->defaults ?? [] ) ]; - $routes[] = Route::fromConfiguration($configuration); + $routesByClassName[$className][] = $configuration; } } } } - return Routes::create(...$routes); + + return $routesByClassName; } } 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()); }