From dcfa5e23e5ba10a0d262118bb5ec52a517ffd196 Mon Sep 17 00:00:00 2001 From: babaorum Date: Sat, 5 Jan 2019 16:37:43 +0100 Subject: [PATCH] add a way to filter areas on route by annotation --- Annotation/Areas.php | 50 ++++++++ DependencyInjection/Configuration.php | 15 ++- DependencyInjection/NelmioApiDocExtension.php | 14 ++- Routing/FilteredRouteCollectionBuilder.php | 50 +++++++- .../DependencyInjection/ConfigurationTest.php | 45 +++++++- Tests/Functional/Controller/ApiController.php | 10 ++ .../FilteredRouteCollectionBuilderTest.php | 109 +++++++++++++++++- Util/ControllerReflector.php | 2 +- 8 files changed, 278 insertions(+), 17 deletions(-) create mode 100644 Annotation/Areas.php diff --git a/Annotation/Areas.php b/Annotation/Areas.php new file mode 100644 index 0000000..f4ce2f9 --- /dev/null +++ b/Annotation/Areas.php @@ -0,0 +1,50 @@ +areas = $areas; + } + + public function has(string $area): bool + { + return in_array($area, $this->areas, true); + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 0a4df63..20ce3dd 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -48,7 +48,16 @@ final class Configuration implements ConfigurationInterface ->end() ->arrayNode('areas') ->info('Filter the routes that are documented') - ->defaultValue(['default' => ['path_patterns' => [], 'host_patterns' => [], 'documentation' => []]]) + ->defaultValue( + [ + 'default' => [ + 'path_patterns' => [], + 'host_patterns' => [], + 'with_annotation' => false, + 'documentation' => [], + ], + ] + ) ->beforeNormalization() ->ifTrue(function ($v) { return 0 === count($v) || isset($v['path_patterns']) || isset($v['host_patterns']) || isset($v['documentation']); @@ -77,6 +86,10 @@ final class Configuration implements ConfigurationInterface ->example(['^api\.']) ->prototype('scalar')->end() ->end() + ->booleanNode('with_annotation') + ->defaultFalse() + ->info('whether to filter by annotation') + ->end() ->arrayNode('documentation') ->useAttributeAsKey('key') ->defaultValue([]) diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index 0bcb69d..2bb642c 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -100,7 +100,10 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI ->addTag(sprintf('nelmio_api_doc.describer.%s', $area), ['priority' => 990]); unset($areaConfig['documentation']); - if (0 === count($areaConfig['path_patterns']) && 0 === count($areaConfig['host_patterns'])) { + if (0 === count($areaConfig['path_patterns']) + && 0 === count($areaConfig['host_patterns']) + && false === $areaConfig['with_annotation'] + ) { $container->setDefinition(sprintf('nelmio_api_doc.routes.%s', $area), $routesDefinition) ->setPublic(false); } else { @@ -108,7 +111,14 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI ->setPublic(false) ->setFactory([ (new Definition(FilteredRouteCollectionBuilder::class)) - ->addArgument($areaConfig), + ->setArguments( + [ + new Reference('annotation_reader'), + new Reference('nelmio_api_doc.controller_reflector'), + $area, + $areaConfig, + ] + ), 'filter', ]) ->addArgument($routesDefinition); diff --git a/Routing/FilteredRouteCollectionBuilder.php b/Routing/FilteredRouteCollectionBuilder.php index 0575c21..4679e66 100644 --- a/Routing/FilteredRouteCollectionBuilder.php +++ b/Routing/FilteredRouteCollectionBuilder.php @@ -11,24 +11,43 @@ namespace Nelmio\ApiDocBundle\Routing; +use Doctrine\Common\Annotations\Reader; +use Nelmio\ApiDocBundle\Annotation\Areas; +use Nelmio\ApiDocBundle\Util\ControllerReflector; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; final class FilteredRouteCollectionBuilder { + /** @var Reader */ + private $annotationReader; + + /** @var ControllerReflector */ + private $controllerReflector; + + /** @var string */ + private $area; + + /** @var array */ private $options; - public function __construct(array $options = []) - { + public function __construct( + Reader $annotationReader, + ControllerReflector $controllerReflector, + string $area, + array $options = [] + ) { $resolver = new OptionsResolver(); $resolver ->setDefaults([ 'path_patterns' => [], 'host_patterns' => [], + 'with_annotation' => false, ]) ->setAllowedTypes('path_patterns', 'string[]') ->setAllowedTypes('host_patterns', 'string[]') + ->setAllowedTypes('with_annotation', 'boolean') ; if (array_key_exists(0, $options)) { @@ -38,6 +57,9 @@ final class FilteredRouteCollectionBuilder $options = $normalizedOptions; } + $this->annotationReader = $annotationReader; + $this->controllerReflector = $controllerReflector; + $this->area = $area; $this->options = $resolver->resolve($options); } @@ -45,7 +67,7 @@ final class FilteredRouteCollectionBuilder { $filteredRoutes = new RouteCollection(); foreach ($routes->all() as $name => $route) { - if ($this->matchPath($route) && $this->matchHost($route)) { + if ($this->matchPath($route) && $this->matchHost($route) && $this->matchAnnotation($route)) { $filteredRoutes->add($name, $route); } } @@ -74,4 +96,26 @@ final class FilteredRouteCollectionBuilder return 0 === count($this->options['host_patterns']); } + + private function matchAnnotation(Route $route): bool + { + if (false === $this->options['with_annotation']) { + return true; + } + + $method = $this->controllerReflector->getReflectionMethod( + $route->getDefault('_controller') ?? '' + ); + if (null === $method) { + return false; + } + + /** @var null|Areas $areas */ + $areas = $this->annotationReader->getMethodAnnotation( + $method, + Areas::class + ); + + return (null !== $areas) ? $areas->has($this->area) : false; + } } diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 34411a9..068c984 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -22,16 +22,41 @@ class ConfigurationTest extends TestCase $processor = new Processor(); $config = $processor->processConfiguration(new Configuration(), [['areas' => ['path_patterns' => ['/foo']]]]); - $this->assertSame(['default' => ['path_patterns' => ['/foo'], 'host_patterns' => [], 'documentation' => []]], $config['areas']); + $this->assertSame( + [ + 'default' => [ + 'path_patterns' => ['/foo'], + 'host_patterns' => [], + 'with_annotation' => false, + 'documentation' => [], + ], + ], + $config['areas'] + ); } public function testAreas() { $processor = new Processor(); $config = $processor->processConfiguration(new Configuration(), [['areas' => $areas = [ - 'default' => ['path_patterns' => ['/foo'], 'host_patterns' => [], 'documentation' => []], - 'internal' => ['path_patterns' => ['/internal'], 'host_patterns' => ['^swagger\.'], 'documentation' => []], - 'commercial' => ['path_patterns' => ['/internal'], 'host_patterns' => [], 'documentation' => []], + 'default' => [ + 'path_patterns' => ['/foo'], + 'host_patterns' => [], + 'with_annotation' => false, + 'documentation' => [], + ], + 'internal' => [ + 'path_patterns' => ['/internal'], + 'host_patterns' => ['^swagger\.'], + 'with_annotation' => false, + 'documentation' => [], + ], + 'commercial' => [ + 'path_patterns' => ['/internal'], + 'host_patterns' => [], + 'with_annotation' => false, + 'documentation' => [], + ], ]]]); $this->assertSame($areas, $config['areas']); @@ -136,6 +161,16 @@ class ConfigurationTest extends TestCase $processor = new Processor(); $config = $processor->processConfiguration(new Configuration(), [['routes' => ['path_patterns' => ['/foo']]]]); - $this->assertSame(['default' => ['path_patterns' => ['/foo'], 'host_patterns' => [], 'documentation' => []]], $config['areas']); + $this->assertSame( + [ + 'default' => [ + 'path_patterns' => ['/foo'], + 'host_patterns' => [], + 'with_annotation' => false, + 'documentation' => [], + ], + ], + $config['areas'] + ); } } diff --git a/Tests/Functional/Controller/ApiController.php b/Tests/Functional/Controller/ApiController.php index 5e9a2db..c359c0f 100644 --- a/Tests/Functional/Controller/ApiController.php +++ b/Tests/Functional/Controller/ApiController.php @@ -13,6 +13,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller; use FOS\RestBundle\Controller\Annotations\QueryParam; use FOS\RestBundle\Controller\Annotations\RequestParam; +use Nelmio\ApiDocBundle\Annotation\Areas; use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Operation; use Nelmio\ApiDocBundle\Annotation\Security; @@ -211,4 +212,13 @@ class ApiController public function operationsWithOtherAnnotations() { } + + /** + * @Route("/areas/new", methods={"GET", "POST"}) + * + * @Areas({"area", "area2"}) + */ + public function newAreaAction() + { + } } diff --git a/Tests/Routing/FilteredRouteCollectionBuilderTest.php b/Tests/Routing/FilteredRouteCollectionBuilderTest.php index 46eb883..8dfd8c0 100644 --- a/Tests/Routing/FilteredRouteCollectionBuilderTest.php +++ b/Tests/Routing/FilteredRouteCollectionBuilderTest.php @@ -11,8 +11,14 @@ namespace Nelmio\ApiDocBundle\Tests\Routing; +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\Reader; +use Nelmio\ApiDocBundle\Annotation\Areas; use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder; +use Nelmio\ApiDocBundle\Util\ControllerReflector; use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -39,7 +45,15 @@ class FilteredRouteCollectionBuilderTest extends TestCase $routes->add($name, $route); } - $routeBuilder = new FilteredRouteCollectionBuilder($options); + $routeBuilder = new FilteredRouteCollectionBuilder( + new AnnotationReader(), + new ControllerReflector( + new Container(), + $this->createMock(ControllerNameParser::class) + ), + 'areaName', + $options + ); $filteredRoutes = $routeBuilder->filter($routes); $this->assertCount(4, $filteredRoutes); @@ -61,7 +75,15 @@ class FilteredRouteCollectionBuilderTest extends TestCase $routes->add($name, $route); } - $routeBuilder = new FilteredRouteCollectionBuilder($pathPattern); + $routeBuilder = new FilteredRouteCollectionBuilder( + new AnnotationReader(), + new ControllerReflector( + new Container(), + $this->createMock(ControllerNameParser::class) + ), + 'areaName', + $pathPattern + ); $filteredRoutes = $routeBuilder->filter($routes); $this->assertCount(5, $filteredRoutes); @@ -74,7 +96,15 @@ class FilteredRouteCollectionBuilderTest extends TestCase */ public function testFilterWithInvalidOption(array $options) { - new FilteredRouteCollectionBuilder($options); + new FilteredRouteCollectionBuilder( + new AnnotationReader(), + new ControllerReflector( + new Container(), + $this->createMock(ControllerNameParser::class) + ), + 'areaName', + $options + ); } public function getInvalidOptions(): array @@ -87,6 +117,9 @@ class FilteredRouteCollectionBuilderTest extends TestCase [['path_patterns' => [null]]], [['path_patterns' => [new \stdClass()]]], [['path_patterns' => ['^/foo$', 1]]], + [['with_annotation' => ['an array']]], + [['path_patterns' => 'a string']], + [['path_patterns' => 11]], ]; } @@ -102,6 +135,7 @@ class FilteredRouteCollectionBuilderTest extends TestCase 'r7' => new Route('/api/bar/action1', [], [], [], 'www.example.com'), 'r8' => new Route('/admin/bar/action1', [], [], [], 'api.example.com'), 'r9' => new Route('/api/bar/action1', [], [], [], 'api.example.com'), + 'r10' => new Route('/api/areas/new'), ]; } @@ -113,7 +147,15 @@ class FilteredRouteCollectionBuilderTest extends TestCase $routes = new RouteCollection(); $routes->add($name, $route); - $routeBuilder = new FilteredRouteCollectionBuilder($options); + $routeBuilder = new FilteredRouteCollectionBuilder( + new AnnotationReader(), + new ControllerReflector( + new Container(), + $this->createMock(ControllerNameParser::class) + ), + 'area', + $options + ); $filteredRoutes = $routeBuilder->filter($routes); $this->assertCount(1, $filteredRoutes); @@ -127,6 +169,55 @@ class FilteredRouteCollectionBuilderTest extends TestCase ['r3', new Route('/api/foo/action2'), ['path_patterns' => ['^/api/foo/action2$']]], ['r4', new Route('/api/demo'), ['path_patterns' => ['/api/demo']]], ['r9', new Route('/api/bar/action1', [], [], [], 'api.example.com'), ['path_patterns' => ['^/api/'], 'host_patterns' => ['^api\.ex']]], + ['r10', new Route('/api/areas/new'), ['path_patterns' => ['^/api']]], + ]; + } + + /** + * @group test + * @dataProvider getMatchingRoutesWithAnnotation + */ + public function testMatchingRoutesWithAnnotation(string $name, Route $route, array $options = []) + { + $routes = new RouteCollection(); + $routes->add($name, $route); + $area = 'area'; + + $reflectionMethodStub = $this->createMock(\ReflectionMethod::class); + $controllerReflectorStub = $this->createMock(ControllerReflector::class); + $controllerReflectorStub->method('getReflectionMethod')->willReturn($reflectionMethodStub); + + $annotationReader = $this->createMock(Reader::class); + $annotationReader + ->method('getMethodAnnotation') + ->with($reflectionMethodStub, Areas::class) + ->willReturn(new Areas(['value' => [$area]])) + ; + + $routeBuilder = new FilteredRouteCollectionBuilder( + $annotationReader, + $controllerReflectorStub, + $area, + $options + ); + $filteredRoutes = $routeBuilder->filter($routes); + + $this->assertCount(1, $filteredRoutes); + } + + public function getMatchingRoutesWithAnnotation(): array + { + return [ + 'with annotation only' => [ + 'r10', + new Route('/api/areas/new', ['_controller' => 'ApiController::newAreaAction']), + ['with_annotation' => true], + ], + 'with annotation and path patterns' => [ + 'r10', + new Route('/api/areas/new', ['_controller' => 'ApiController::newAreaAction']), + ['path_patterns' => ['^/api'], 'with_annotation' => true], + ], ]; } @@ -138,7 +229,15 @@ class FilteredRouteCollectionBuilderTest extends TestCase $routes = new RouteCollection(); $routes->add($name, $route); - $routeBuilder = new FilteredRouteCollectionBuilder($options); + $routeBuilder = new FilteredRouteCollectionBuilder( + new AnnotationReader(), + new ControllerReflector( + new Container(), + $this->createMock(ControllerNameParser::class) + ), + 'areaName', + $options + ); $filteredRoutes = $routeBuilder->filter($routes); $this->assertCount(0, $filteredRoutes); diff --git a/Util/ControllerReflector.php b/Util/ControllerReflector.php index 3ea247f..568da23 100644 --- a/Util/ControllerReflector.php +++ b/Util/ControllerReflector.php @@ -17,7 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * @internal */ -final class ControllerReflector +class ControllerReflector { private $container;