mirror of
https://github.com/retailcrm/NelmioApiDocBundle.git
synced 2025-02-02 23:59:26 +03:00
add a way to filter areas on route by annotation
This commit is contained in:
parent
0bca7f377d
commit
dcfa5e23e5
50
Annotation/Areas.php
Normal file
50
Annotation/Areas.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Annotation;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
*/
|
||||
final class Areas
|
||||
{
|
||||
/** @var string[] */
|
||||
private $areas;
|
||||
|
||||
public function __construct(array $properties)
|
||||
{
|
||||
if (!array_key_exists('value', $properties) || !is_array($properties['value'])) {
|
||||
throw new \InvalidArgumentException('An array of areas was expected');
|
||||
}
|
||||
|
||||
$areas = [];
|
||||
foreach ($properties['value'] as $area) {
|
||||
if (!is_string($area)) {
|
||||
throw new \InvalidArgumentException('An area must be given as a string');
|
||||
}
|
||||
|
||||
if (!in_array($area, $areas)) {
|
||||
$areas[] = $area;
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === count($areas)) {
|
||||
throw new \LogicException('At least one area is expected');
|
||||
}
|
||||
|
||||
$this->areas = $areas;
|
||||
}
|
||||
|
||||
public function has(string $area): bool
|
||||
{
|
||||
return in_array($area, $this->areas, true);
|
||||
}
|
||||
}
|
@ -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([])
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -17,7 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ControllerReflector
|
||||
class ControllerReflector
|
||||
{
|
||||
private $container;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user