From 1733d9e67ced2e5415b01729016beaf66e16bbe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Tue, 11 Jan 2022 00:43:23 +0100 Subject: [PATCH] Add route attributes factory --- .../Resources/config/services/routing.xml | 4 + .../Routing/CrudRoutesAttributesLoader.php | 36 +-- src/Bundle/Routing/RouteAttributesFactory.php | 75 ++++++ .../RouteAttributesFactoryInterface.php | 24 ++ src/Bundle/Routing/RoutesAttributesLoader.php | 76 +----- .../Routing/RouteAttributesFactorySpec.php | 236 ++++++++++++++++++ .../Routing/RoutesAttributesLoaderSpec.php | 162 +----------- .../src/Entity/Route/ShowBookWithPriority.php | 6 +- src/Component/Annotation/SyliusCrudRoutes.php | 2 +- src/Component/Annotation/SyliusRoute.php | 2 +- src/Component/Reflection/ClassReflection.php | 28 ++- 11 files changed, 395 insertions(+), 256 deletions(-) create mode 100644 src/Bundle/Routing/RouteAttributesFactory.php create mode 100644 src/Bundle/Routing/RouteAttributesFactoryInterface.php create mode 100644 src/Bundle/spec/Routing/RouteAttributesFactorySpec.php diff --git a/src/Bundle/Resources/config/services/routing.xml b/src/Bundle/Resources/config/services/routing.xml index 7f0585575..2074f4ad2 100644 --- a/src/Bundle/Resources/config/services/routing.xml +++ b/src/Bundle/Resources/config/services/routing.xml @@ -31,7 +31,11 @@ %sylius.resource.mapping% + + + + diff --git a/src/Bundle/Routing/CrudRoutesAttributesLoader.php b/src/Bundle/Routing/CrudRoutesAttributesLoader.php index 7f0062a75..95c6b285c 100644 --- a/src/Bundle/Routing/CrudRoutesAttributesLoader.php +++ b/src/Bundle/Routing/CrudRoutesAttributesLoader.php @@ -29,44 +29,24 @@ public function __construct( public function __invoke(): RouteCollection { $routeCollection = new RouteCollection(); + $paths = $this->mapping['paths'] ?? []; - /** @var \ReflectionClass $reflectionClass */ - foreach ($this->getReflectionClasses() as $reflectionClass) { - $this->addRoutesForSyliusCrudRoutesAttributes($routeCollection, $reflectionClass); + /** @var string $className */ + foreach (ClassReflection::getResourcesByPaths($paths) as $className) { + $this->addRoutesForSyliusCrudRoutesAttributes($routeCollection, $className); } return $routeCollection; } - private function addRoutesForSyliusCrudRoutesAttributes(RouteCollection $routeCollection, \ReflectionClass $reflectionClass): void + private function addRoutesForSyliusCrudRoutesAttributes(RouteCollection $routeCollection, string $className): void { - foreach ($this->getClassAttributes($reflectionClass, SyliusCrudRoutes::class) as $reflectionAttribute) { + $attributes = ClassReflection::getClassAttributes($className, SyliusCrudRoutes::class); + + foreach ($attributes as $reflectionAttribute) { $resource = Yaml::dump($reflectionAttribute->getArguments()); $resourceRouteCollection = $this->resourceLoader->load($resource); $routeCollection->addCollection($resourceRouteCollection); } } - - /** - * @return \ReflectionAttribute[] - */ - private function getClassAttributes(\ReflectionClass $reflectionClass, string $attributeName): array - { - return $reflectionClass->getAttributes($attributeName); - } - - private function getReflectionClasses(): iterable - { - $paths = $this->mapping['paths'] ?? []; - - foreach ($paths as $resourceDirectory) { - $resources = ClassReflection::getResourcesByPath($resourceDirectory); - - foreach ($resources as $className) { - $reflectionClass = new \ReflectionClass($className); - - yield $className => $reflectionClass; - } - } - } } diff --git a/src/Bundle/Routing/RouteAttributesFactory.php b/src/Bundle/Routing/RouteAttributesFactory.php new file mode 100644 index 000000000..49abe7c98 --- /dev/null +++ b/src/Bundle/Routing/RouteAttributesFactory.php @@ -0,0 +1,75 @@ +getArguments(); + + Assert::keyExists($arguments, 'name', 'Your route should have a name attribute.'); + + $syliusOptions = []; + + if (isset($arguments['template'])) { + $syliusOptions['template'] = $arguments['template']; + } + + if (isset($arguments['vars'])) { + $syliusOptions['vars'] = $arguments['vars']; + } + + if (isset($arguments['criteria'])) { + $syliusOptions['criteria'] = $arguments['criteria']; + } + + if (isset($arguments['repository'])) { + $syliusOptions['repository'] = $arguments['repository']; + } + + if (isset($arguments['serializationGroups'])) { + $syliusOptions['serialization_groups'] = $arguments['serializationGroups']; + } + + if (isset($arguments['serializationVersion'])) { + $syliusOptions['serialization_version'] = $arguments['serializationVersion']; + } + + $route = new Route( + $arguments['path'], + [ + '_controller' => $arguments['controller'] ?? null, + '_sylius' => $syliusOptions, + ], + $arguments['requirements'] ?? [], + $arguments['options'] ?? [], + $arguments['host'] ?? '', + $arguments['schemes'] ?? [], + $arguments['methods'] ?? [] + ); + + $routeCollection->add($arguments['name'], $route, $arguments['priority'] ?? 0); + } + } +} diff --git a/src/Bundle/Routing/RouteAttributesFactoryInterface.php b/src/Bundle/Routing/RouteAttributesFactoryInterface.php new file mode 100644 index 000000000..495a606aa --- /dev/null +++ b/src/Bundle/Routing/RouteAttributesFactoryInterface.php @@ -0,0 +1,24 @@ +mapping = $mapping; + $this->routesAttributesFactory = $routesAttributesFactory; } public function __invoke(): RouteCollection { $routeCollection = new RouteCollection(); + $paths = $this->mapping['paths'] ?? []; - /** @var \ReflectionClass $reflectionClass */ - foreach ($this->getReflectionClasses() as $reflectionClass) { - $this->addRoutesForSyliusRouteAttributes($routeCollection, $reflectionClass); + /** @var string $className */ + foreach ($paths as $className) { + $this->routesAttributesFactory->createRouteForClass($routeCollection, $className); } return $routeCollection; } - private function addRoutesForSyliusRouteAttributes(RouteCollection $routeCollection, \ReflectionClass $reflectionClass): void - { - foreach ($this->getClassAttributes($reflectionClass, SyliusRoute::class) as $reflectionAttribute) { - $arguments = $reflectionAttribute->getArguments(); - - Assert::keyExists($arguments, 'name', 'Your route should have a name attribute.'); - - $syliusOptions = []; - - if (isset($arguments['template'])) { - $syliusOptions['template'] = $arguments['template']; - } - - if (isset($arguments['vars'])) { - $syliusOptions['vars'] = $arguments['vars']; - } - - if (isset($arguments['criteria'])) { - $syliusOptions['criteria'] = $arguments['criteria']; - } - - if (isset($arguments['repository'])) { - $syliusOptions['repository'] = $arguments['repository']; - } - - if (isset($arguments['serializationGroups'])) { - $syliusOptions['serialization_groups'] = $arguments['serializationGroups']; - } - - if (isset($arguments['serializationVersion'])) { - $syliusOptions['serialization_version'] = $arguments['serializationVersion']; - } - - $route = new Route( - $arguments['path'], - [ - '_controller' => $arguments['controller'] ?? null, - '_sylius' => $syliusOptions, - ], - $arguments['requirements'] ?? [], - $arguments['options'] ?? [], - $arguments['host'] ?? '', - $arguments['schemes'] ?? [], - $arguments['methods'] ?? [] - ); - - $routeCollection->add($arguments['name'], $route, $arguments['priority'] ?? 0); - } - } - - /** - * @return \ReflectionAttribute[] - */ - private function getClassAttributes(\ReflectionClass $reflectionClass, string $attributeName): array - { - return $reflectionClass->getAttributes($attributeName); - } - - private function getReflectionClasses(): iterable + private function getClasses(): iterable { $paths = $this->mapping['paths'] ?? []; @@ -106,9 +52,7 @@ private function getReflectionClasses(): iterable $resources = ClassReflection::getResourcesByPath($resourceDirectory); foreach ($resources as $className) { - $reflectionClass = new \ReflectionClass($className); - - yield $className => $reflectionClass; + yield $className; } } } diff --git a/src/Bundle/spec/Routing/RouteAttributesFactorySpec.php b/src/Bundle/spec/Routing/RouteAttributesFactorySpec.php new file mode 100644 index 000000000..664259522 --- /dev/null +++ b/src/Bundle/spec/Routing/RouteAttributesFactorySpec.php @@ -0,0 +1,236 @@ +shouldHaveType(RouteAttributesFactory::class); + } + + function it_generates_routes_from_resource(): void { + $routeCollection = new RouteCollection(); + + $this->createRouteForClass($routeCollection, ShowBook::class); + + $route = $routeCollection->get('show_book'); + + Assert::eq($route->getPath(), '/book/{id}'); + Assert::eq($route->getDefaults(), [ + '_controller' => 'app.controller.book:showAction', + '_sylius' => [], + ]); + } + + function it_generates_routes_from_resource_with_methods(): void + { + $routeCollection = new RouteCollection(); + + $this->createRouteForClass($routeCollection, ShowBookWithMethods::class); + + $route = $routeCollection->get('show_book_with_methods'); + Assert::eq($route->getPath(), '/book/{id}'); + Assert::eq($route->getMethods(), ['GET']); + Assert::eq($route->getDefaults(), [ + '_controller' => 'app.controller.book:showAction', + '_sylius' => [], + ]); + } + + function it_generates_routes_from_resource_with_criteria(): void + { + $routeCollection = new RouteCollection(); + + $this->createRouteForClass($routeCollection, ShowBookWithCriteria::class); + + $route = $routeCollection->get('show_book_with_criteria'); + Assert::eq($route->getPath(), '/library/{libraryId}/book/{id}'); + Assert::eq($route->getDefaults(), [ + '_controller' => 'app.controller.book:showAction', + '_sylius' => [ + 'criteria' => [ + 'library' => '$libraryId', + ], + ], + ]); + } + + function it_generates_routes_from_resource_with_template(): void + { + $routeCollection = new RouteCollection(); + + $this->createRouteForClass($routeCollection, ShowBookWithTemplate::class); + + $route = $routeCollection->get('show_book_with_template'); + Assert::eq($route->getPath(), '/book/{id}'); + Assert::eq($route->getDefaults(), [ + '_controller' => 'app.controller.book:showAction', + '_sylius' => [ + 'template' => 'book/show.html.twig', + ], + ]); + } + + function it_generates_routes_from_resource_with_repository(): void + { + $routeCollection = new RouteCollection(); + + $this->createRouteForClass($routeCollection, ShowBookWithRepository::class); + + $route = $routeCollection->get('show_book_with_repository'); + Assert::eq($route->getPath(), '/book/{id}'); + Assert::eq($route->getDefaults(), [ + '_controller' => 'app.controller.book:showAction', + '_sylius' => [ + 'repository' => [ + 'method' => 'findOneNewestByAuthor', + 'arguments' => '[$author]', + ], + ], + ]); + } + + function it_generates_routes_from_resource_with_serialization_groups(): void + { + $routeCollection = new RouteCollection(); + + $this->createRouteForClass($routeCollection, ShowBookWithSerializationGroups::class); + + $route = $routeCollection->get('show_book_with_serialization_groups'); + Assert::eq($route->getPath(), '/book/{id}'); + Assert::eq($route->getDefaults(), [ + '_controller' => 'app.controller.book:showAction', + '_sylius' => [ + 'serialization_groups' => ['sylius'], + ], + ]); + } + + function it_generates_routes_from_resource_with_serialization_version(): void { + $routeCollection = new RouteCollection(); + + $this->createRouteForClass($routeCollection, ShowBookWithSerializationVersion::class); + + $route = $routeCollection->get('show_book_with_serialization_version'); + Assert::eq($route->getPath(), '/book/{id}'); + Assert::eq($route->getDefaults(), [ + '_controller' => 'app.controller.book:showAction', + '_sylius' => [ + 'serialization_version' => '1.0', + ], + ]); + } + + function it_generates_routes_from_resource_with_vars(): void { + $routeCollection = new RouteCollection(); + + $this->createRouteForClass($routeCollection, ShowBookWithVars::class); + + $route = $routeCollection->get('show_book_with_vars'); + Assert::eq($route->getPath(), '/book/{id}'); + Assert::eq($route->getDefaults(), [ + '_controller' => 'app.controller.book:showAction', + '_sylius' => [ + 'vars' => [ + 'foo' => 'bar', + ], + ], + ]); + } + + function it_generates_routes_from_resource_with_requirements(): void + { + $routeCollection = new RouteCollection(); + + $this->createRouteForClass($routeCollection, ShowBookWithRequirements::class); + + $route = $routeCollection->get('show_book_with_requirements'); + Assert::eq($route->getPath(), '/book/{id}'); + Assert::eq($route->getRequirements(), [ + 'id' => '\d+', + ]); + } + + function it_generates_routes_from_resource_with_priority(): void + { + if (Kernel::MAJOR_VERSION < 5) { + return; + } + + $routeCollection = new RouteCollection(); + + $this->createRouteForClass($routeCollection, ShowBookWithPriority::class); + + Assert::eq($routeCollection->count(), 2); + $route = $routeCollection->get('show_book_with_priority'); + Assert::eq($route->getPath(), '/book/{id}'); + Assert::eq($routeCollection->getIterator()->current(), $route); + } + + function it_generates_routes_from_resource_with_options(): void + { + $routeCollection = new RouteCollection(); + + $this->createRouteForClass($routeCollection, ShowBookWithOptions::class); + + $route = $routeCollection->get('show_book_with_options'); + Assert::eq($route->getPath(), '/book/{id}'); + Assert::eq($route->getOptions(), [ + 'compiler_class' => RouteCompiler::class, + 'utf8' => true, + ]); + } + + function it_generates_routes_from_resource_with_host(): void + { + $routeCollection = new RouteCollection(); + + $this->createRouteForClass($routeCollection, ShowBookWithHost::class); + + $route = $routeCollection->get('show_book_with_host'); + Assert::eq($route->getPath(), '/book/{id}'); + Assert::eq($route->getHost(), 'm.example.com'); + } + + function it_generates_routes_from_resource_with_schemes(): void + { + $routeCollection = new RouteCollection(); + + $this->createRouteForClass($routeCollection, ShowBookWithSchemes::class); + + $route = $routeCollection->get('show_book_with_schemes'); + Assert::eq($route->getPath(), '/book/{id}'); + Assert::eq($route->getSchemes(), ['https']); + } +} diff --git a/src/Bundle/spec/Routing/RoutesAttributesLoaderSpec.php b/src/Bundle/spec/Routing/RoutesAttributesLoaderSpec.php index d78d8d6de..c34b87775 100644 --- a/src/Bundle/spec/Routing/RoutesAttributesLoaderSpec.php +++ b/src/Bundle/spec/Routing/RoutesAttributesLoaderSpec.php @@ -14,15 +14,16 @@ namespace spec\Sylius\Bundle\ResourceBundle\Routing; use PhpSpec\ObjectBehavior; +use Prophecy\Argument; +use Sylius\Bundle\ResourceBundle\Routing\RouteAttributesFactoryInterface; use Sylius\Bundle\ResourceBundle\Routing\RoutesAttributesLoader; -use Symfony\Component\HttpKernel\Kernel; -use Symfony\Component\Routing\RouteCompiler; +use Symfony\Component\Routing\RouteCollection; final class RoutesAttributesLoaderSpec extends ObjectBehavior { - function let(): void + function let(RouteAttributesFactoryInterface $routeAttributesFactory): void { - $this->beConstructedWith(['paths' => [__DIR__.'/../../test/src']]); + $this->beConstructedWith(['paths' => [__DIR__.'/../../test/src/Entity/Route']], $routeAttributesFactory); } function it_is_initializable(): void @@ -30,157 +31,10 @@ function it_is_initializable(): void $this->shouldHaveType(RoutesAttributesLoader::class); } - function it_generates_routes_from_resource(): void { - $routes = $this->__invoke(); - $route = $routes->get('show_book'); - $route->getPath()->shouldReturn('/book/{id}'); - $route->getDefaults()->shouldReturn([ - '_controller' => 'app.controller.book:showAction', - '_sylius' => [], - ]); - } - - function it_generates_routes_from_resource_with_methods(): void - { - $routes = $this->__invoke(); - $route = $routes->get('show_book_with_methods'); - $route->getPath()->shouldReturn('/book/{id}'); - $route->getMethods()->shouldReturn(['GET']); - $route->getDefaults()->shouldReturn([ - '_controller' => 'app.controller.book:showAction', - '_sylius' => [], - ]); - } - - function it_generates_routes_from_resource_with_criteria(): void + function it_generates_routes_from_paths(RouteAttributesFactoryInterface $routeAttributesFactory): void { - $routes = $this->__invoke(); - $route = $routes->get('show_book_with_criteria'); - $route->getPath()->shouldReturn('/library/{libraryId}/book/{id}'); - $route->getDefaults()->shouldReturn([ - '_controller' => 'app.controller.book:showAction', - '_sylius' => [ - 'criteria' => [ - 'library' => '$libraryId', - ], - ], - ]); - } - - function it_generates_routes_from_resource_with_template(): void - { - $routes = $this->__invoke(); - $route = $routes->get('show_book_with_template'); - $route->getPath()->shouldReturn('/book/{id}'); - $route->getDefaults()->shouldReturn([ - '_controller' => 'app.controller.book:showAction', - '_sylius' => [ - 'template' => 'book/show.html.twig', - ], - ]); - } - - function it_generates_routes_from_resource_with_repository(): void - { - $routes = $this->__invoke(); - $route = $routes->get('show_book_with_repository'); - $route->getPath()->shouldReturn('/book/{id}'); - $route->getDefaults()->shouldReturn([ - '_controller' => 'app.controller.book:showAction', - '_sylius' => [ - 'repository' => [ - 'method' => 'findOneNewestByAuthor', - 'arguments' => '[$author]', - ], - ], - ]); - } - - function it_generates_routes_from_resource_with_serialization_groups(): void - { - $routes = $this->__invoke(); - $route = $routes->get('show_book_with_serialization_groups'); - $route->getPath()->shouldReturn('/book/{id}'); - $route->getDefaults()->shouldReturn([ - '_controller' => 'app.controller.book:showAction', - '_sylius' => [ - 'serialization_groups' => ['sylius'], - ], - ]); - } - - function it_generates_routes_from_resource_with_serialization_version(): void { - $routes = $this->__invoke(); - $route = $routes->get('show_book_with_serialization_version'); - $route->getPath()->shouldReturn('/book/{id}'); - $route->getDefaults()->shouldReturn([ - '_controller' => 'app.controller.book:showAction', - '_sylius' => [ - 'serialization_version' => '1.0', - ], - ]); - } + $routeAttributesFactory->createRouteForClass(Argument::type(RouteCollection::class), Argument::type('string'))->shouldBeCalledTimes(13); - function it_generates_routes_from_resource_with_vars(): void { - $routes = $this->__invoke(); - $route = $routes->get('show_book_with_vars'); - $route->getPath()->shouldReturn('/book/{id}'); - $route->getDefaults()->shouldReturn([ - '_controller' => 'app.controller.book:showAction', - '_sylius' => [ - 'vars' => [ - 'foo' => 'bar', - ], - ], - ]); - } - - function it_generates_routes_from_resource_with_requirements(): void - { - $routes = $this->__invoke(); - $route = $routes->get('show_book_with_requirements'); - $route->getPath()->shouldReturn('/book/{id}'); - $route->getRequirements()->shouldReturn([ - 'id' => '\d+', - ]); - } - - function it_generates_routes_from_resource_with_priority(): void - { - if (Kernel::MAJOR_VERSION < 5) { - return; - } - - $routes = $this->__invoke(); - $route = $routes->get('show_book_with_priority'); - $route->getPath()->shouldReturn('/book/{id}'); - $routes->getIterator()->current()->shouldReturn($route); - } - - function it_generates_routes_from_resource_with_options(): void - { - $routes = $this->__invoke(); - $route = $routes->get('show_book_with_options'); - $route->getPath()->shouldReturn('/book/{id}'); - $route->getOptions()->shouldReturn([ - 'compiler_class' => RouteCompiler::class, - 'utf8' => true, - ]); - } - - function it_generates_routes_from_resource_with_host(): void - { - $routes = $this->__invoke(); - $route = $routes->get('show_book_with_host'); - $route->getPath()->shouldReturn('/book/{id}'); - $route->getHost()->shouldReturn('m.example.com'); - } - - function it_generates_routes_from_resource_with_schemes(): void - { - $routes = $this->__invoke(); - $route = $routes->get('show_book_with_schemes'); - $route->getPath()->shouldReturn('/book/{id}'); - $route->getSchemes()->shouldReturn(['https']); + $this->__invoke(); } } diff --git a/src/Bundle/test/src/Entity/Route/ShowBookWithPriority.php b/src/Bundle/test/src/Entity/Route/ShowBookWithPriority.php index 343cf7786..e71e4e8a2 100644 --- a/src/Bundle/test/src/Entity/Route/ShowBookWithPriority.php +++ b/src/Bundle/test/src/Entity/Route/ShowBookWithPriority.php @@ -15,12 +15,16 @@ use App\Entity\Book; use JMS\Serializer\Annotation as Serializer; -use RectorPrefix20210817\template; use Sylius\Component\Resource\Annotation\SyliusRoute; /** * @Serializer\ExclusionPolicy("all") */ +#[SyliusRoute( + name: 'show_book_without_priority', + path: '/book/{id}', + controller: 'app.controller.book:showAction', +)] #[SyliusRoute( name: 'show_book_with_priority', path: '/book/{id}', diff --git a/src/Component/Annotation/SyliusCrudRoutes.php b/src/Component/Annotation/SyliusCrudRoutes.php index 66cea5934..d67996fc1 100644 --- a/src/Component/Annotation/SyliusCrudRoutes.php +++ b/src/Component/Annotation/SyliusCrudRoutes.php @@ -13,7 +13,7 @@ namespace Sylius\Component\Resource\Annotation; -#[\Attribute(\Attribute::TARGET_CLASS)] +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] final class SyliusCrudRoutes { public ?string $alias = null; diff --git a/src/Component/Annotation/SyliusRoute.php b/src/Component/Annotation/SyliusRoute.php index 32e62876e..e4a8db7a9 100644 --- a/src/Component/Annotation/SyliusRoute.php +++ b/src/Component/Annotation/SyliusRoute.php @@ -13,7 +13,7 @@ namespace Sylius\Component\Resource\Annotation; -#[\Attribute(\Attribute::TARGET_CLASS)] +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] final class SyliusRoute { public ?string $name = null; diff --git a/src/Component/Reflection/ClassReflection.php b/src/Component/Reflection/ClassReflection.php index 78a122ce4..e79ae6d02 100644 --- a/src/Component/Reflection/ClassReflection.php +++ b/src/Component/Reflection/ClassReflection.php @@ -17,11 +17,21 @@ final class ClassReflection { - public static function getResourcesByPath(string $path): array + public static function getResourcesByPaths(array $paths): iterable + { + foreach ($paths as $resourceDirectory) { + $resources = ClassReflection::getResourcesByPath($resourceDirectory); + + foreach ($resources as $className) { + yield $className; + } + } + } + + public static function getResourcesByPath(string $path): iterable { $finder = new Finder(); $finder->files()->in($path)->name('*.php')->sortByName(true); - $classes = []; foreach ($finder as $file) { $fileContent = (string) file_get_contents((string) $file->getRealPath()); @@ -38,12 +48,20 @@ public static function getResourcesByPath(string $path): array $className = trim($matches[1]); if (null !== $namespace) { - $classes[] = $namespace.'\\'.$className; + yield $namespace.'\\'.$className; } else { - $classes[] = $className; + yield $className; } } + } + + /** + * @return \ReflectionAttribute[] + */ + public static function getClassAttributes(string $className, string $attributeName): array + { + $reflectionClass = new \ReflectionClass($className); - return $classes; + return $reflectionClass->getAttributes($attributeName); } }