Merge pull request #1257 from phansys/ticket_1121

Allow to filter routes by host (#1121)
This commit is contained in:
David Buchmann 2018-03-17 15:07:31 +01:00 committed by GitHub
commit 3698daa547
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 200 additions and 29 deletions

View File

@ -7,6 +7,16 @@ CHANGELOG
* Add a documentation form extension. Use the ``documentation`` option to define how a form field is documented.
* Allow references to config definitions in controllers.
Config
* `nelmio_api_doc.areas` added support to filter by host patterns.
```yml
nelmio_api_doc:
routes: [ host_patterns: [ ^api\. ] ]
```
* Added dependency for "symfony/options-resolver:^3.4.4|^4.0"
3.1.0 (2017-01-28)
------------------

View File

@ -48,10 +48,10 @@ final class Configuration implements ConfigurationInterface
->end()
->arrayNode('areas')
->info('Filter the routes that are documented')
->defaultValue(['default' => ['path_patterns' => []]])
->defaultValue(['default' => ['path_patterns' => [], 'host_patterns' => []]])
->beforeNormalization()
->ifTrue(function ($v) {
return empty($v) or isset($v['path_patterns']);
return 0 === count($v) || isset($v['path_patterns']) || isset($v['host_patterns']);
})
->then(function ($v) {
return ['default' => $v];
@ -68,9 +68,15 @@ final class Configuration implements ConfigurationInterface
->addDefaultsIfNotSet()
->children()
->arrayNode('path_patterns')
->defaultValue([])
->example(['^/api', '^/api(?!/admin)'])
->prototype('scalar')->end()
->end()
->arrayNode('host_patterns')
->defaultValue([])
->example(['^api\.'])
->prototype('scalar')->end()
->end()
->end()
->end()
->end()

View File

@ -66,7 +66,7 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
new TaggedIteratorArgument('nelmio_api_doc.model_describer'),
]);
if (0 === count($areaConfig['path_patterns'])) {
if (0 === count($areaConfig['path_patterns']) && 0 === count($areaConfig['host_patterns'])) {
$container->setDefinition(sprintf('nelmio_api_doc.routes.%s', $area), $routesDefinition)
->setPublic(false);
} else {
@ -74,7 +74,7 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
->setPublic(false)
->setFactory([
(new Definition(FilteredRouteCollectionBuilder::class))
->addArgument($areaConfig['path_patterns']),
->addArgument($areaConfig),
'filter',
])
->addArgument($routesDefinition);

View File

@ -8,6 +8,7 @@ We've already seen that you can configure which routes are documented using ``ne
nelmio_api_doc:
areas:
path_patterns: [ ^/api ]
host_patterns: [ ^api\. ]
But in fact, this config option is way more powerful and allows you to split your documentation in several parts.
@ -22,6 +23,7 @@ You can define areas which will each generates a different documentation:
areas:
default:
path_patterns: [ ^/api ]
host_patterns: [ ^api\. ]
internal:
path_patterns: [ ^/internal ]
commercial:

View File

@ -81,6 +81,8 @@ Open a command console, enter your project directory and execute the following c
areas:
path_patterns: # an array of regexps
- ^/api(?!/doc$)
host_patterns:
- ^api\.
How does this bundle work?
--------------------------

View File

@ -11,23 +11,41 @@
namespace Nelmio\ApiDocBundle\Routing;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
final class FilteredRouteCollectionBuilder
{
private $pathPatterns;
private $options;
public function __construct(array $pathPatterns = [])
public function __construct(array $options = [])
{
$this->pathPatterns = $pathPatterns;
$resolver = new OptionsResolver();
$resolver
->setDefaults([
'path_patterns' => [],
'host_patterns' => [],
])
->setAllowedTypes('path_patterns', 'string[]')
->setAllowedTypes('host_patterns', 'string[]')
;
if (array_key_exists(0, $options)) {
@trigger_error(sprintf('Passing an indexed array with a collection of path patterns as argument 1 for `%s()` is deprecated since 3.2.0, expected structure is an array containing parameterized options.', __METHOD__), E_USER_DEPRECATED);
$normalizedOptions = ['path_patterns' => $options];
$options = $normalizedOptions;
}
$this->options = $resolver->resolve($options);
}
public function filter(RouteCollection $routes): RouteCollection
{
$filteredRoutes = new RouteCollection();
foreach ($routes->all() as $name => $route) {
if ($this->match($route)) {
if ($this->matchPath($route) && $this->matchHost($route)) {
$filteredRoutes->add($name, $route);
}
}
@ -35,14 +53,25 @@ final class FilteredRouteCollectionBuilder
return $filteredRoutes;
}
private function match(Route $route): bool
private function matchPath(Route $route): bool
{
foreach ($this->pathPatterns as $pathPattern) {
foreach ($this->options['path_patterns'] as $pathPattern) {
if (preg_match('{'.$pathPattern.'}', $route->getPath())) {
return true;
}
}
return false;
return 0 === count($this->options['path_patterns']);
}
private function matchHost(Route $route): bool
{
foreach ($this->options['host_patterns'] as $hostPattern) {
if (preg_match('{'.$hostPattern.'}', $route->getHost())) {
return true;
}
}
return 0 === count($this->options['host_patterns']);
}
}

View File

@ -22,19 +22,19 @@ class ConfigurationTest extends TestCase
$processor = new Processor();
$config = $processor->processConfiguration(new Configuration(), [['areas' => ['path_patterns' => ['/foo']]]]);
$this->assertEquals(['default' => ['path_patterns' => ['/foo']]], $config['areas']);
$this->assertSame(['default' => ['path_patterns' => ['/foo'], 'host_patterns' => []]], $config['areas']);
}
public function testAreas()
{
$processor = new Processor();
$config = $processor->processConfiguration(new Configuration(), [['areas' => $areas = [
'default' => ['path_patterns' => ['/foo']],
'internal' => ['path_patterns' => ['/internal']],
'commercial' => ['path_patterns' => ['/internal']],
'default' => ['path_patterns' => ['/foo'], 'host_patterns' => []],
'internal' => ['path_patterns' => ['/internal'], 'host_patterns' => ['^swagger\.']],
'commercial' => ['path_patterns' => ['/internal'], 'host_patterns' => []],
]]]);
$this->assertEquals($areas, $config['areas']);
$this->assertSame($areas, $config['areas']);
}
/**
@ -57,6 +57,6 @@ class ConfigurationTest extends TestCase
$processor = new Processor();
$config = $processor->processConfiguration(new Configuration(), [['routes' => ['path_patterns' => ['/foo']]]]);
$this->assertEquals(['default' => ['path_patterns' => ['/foo']]], $config['areas']);
$this->assertSame(['default' => ['path_patterns' => ['/foo'], 'host_patterns' => []]], $config['areas']);
}
}

View File

@ -25,7 +25,7 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Swagger\Annotations as SWG;
/**
* @Route("/api")
* @Route("/api", host="api.example.com")
*/
class ApiController
{

View File

@ -18,6 +18,9 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\VirtualProperty;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Swagger\Annotations as SWG;
/**
* @Route(host="api.example.com")
*/
class JMSController
{
/**

View File

@ -15,7 +15,7 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Swagger\Annotations as SWG;
/**
* @Route("/test")
* @Route("/test", host="api-test.example.com")
*/
class TestController
{

View File

@ -13,6 +13,9 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
/**
* @Route(host="api.example.com")
*/
class UndocumentedController
{
/**

View File

@ -15,7 +15,7 @@ class SwaggerUiTest extends WebTestCase
{
protected static function createClient(array $options = [], array $server = [])
{
return parent::createClient([], ['PHP_SELF' => '/app_dev.php/docs', 'SCRIPT_FILENAME' => '/var/www/app/web/app_dev.php']);
return parent::createClient([], $server + ['HTTP_HOST' => 'api.example.com', 'PHP_SELF' => '/app_dev.php/docs', 'SCRIPT_FILENAME' => '/var/www/app/web/app_dev.php']);
}
/**

View File

@ -116,8 +116,8 @@ class TestKernel extends Kernel
],
],
'areas' => [
'default' => ['path_patterns' => ['^/api(?!/admin)']],
'test' => ['path_patterns' => ['^/test']],
'default' => ['path_patterns' => ['^/api(?!/admin)'], 'host_patterns' => ['^api\.']],
'test' => ['path_patterns' => ['^/test'], 'host_patterns' => ['^api-test\.']],
],
]);
}

View File

@ -24,7 +24,7 @@ class WebTestCase extends BaseWebTestCase
protected function getSwaggerDefinition()
{
static::createClient();
static::createClient([], ['HTTP_HOST' => 'api.example.com']);
return static::$kernel->getContainer()->get('nelmio_api_doc.generator')->generate();
}

View File

@ -22,6 +22,34 @@ use Symfony\Component\Routing\RouteCollection;
class FilteredRouteCollectionBuilderTest extends TestCase
{
public function testFilter()
{
$options = [
'path_patterns' => [
'^/api/foo',
'^/api/bar',
],
'host_patterns' => [
'^$',
'^api\.',
],
];
$routes = new RouteCollection();
foreach ($this->getRoutes() as $name => $route) {
$routes->add($name, $route);
}
$routeBuilder = new FilteredRouteCollectionBuilder($options);
$filteredRoutes = $routeBuilder->filter($routes);
$this->assertCount(4, $filteredRoutes);
}
/**
* @group legacy
* @expectedDeprecation Passing an indexed array with a collection of path patterns as argument 1 for `Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder::__construct()` is deprecated since 3.2.0, expected structure is an array containing parameterized options.
*/
public function testFilterWithDeprecatedArgument()
{
$pathPattern = [
'^/api/foo',
@ -29,15 +57,102 @@ class FilteredRouteCollectionBuilderTest extends TestCase
];
$routes = new RouteCollection();
$routes->add('r1', new Route('/api/bar/action1'));
$routes->add('r2', new Route('/api/foo/action1'));
$routes->add('r3', new Route('/api/foo/action2'));
$routes->add('r4', new Route('/api/demo'));
$routes->add('r5', new Route('/_profiler/test/test'));
foreach ($this->getRoutes() as $name => $route) {
$routes->add($name, $route);
}
$routeBuilder = new FilteredRouteCollectionBuilder($pathPattern);
$filteredRoutes = $routeBuilder->filter($routes);
$this->assertCount(3, $filteredRoutes);
$this->assertCount(5, $filteredRoutes);
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidArgumentException
*
* @dataProvider getInvalidOptions
*/
public function testFilterWithInvalidOption(array $options)
{
new FilteredRouteCollectionBuilder($options);
}
public function getInvalidOptions(): array
{
return [
[['invalid_option' => null]],
[['invalid_option' => 42]],
[['invalid_option' => []]],
[['path_patterns' => [22]]],
[['path_patterns' => [null]]],
[['path_patterns' => [new \stdClass()]]],
[['path_patterns' => ['^/foo$', 1]]],
];
}
private function getRoutes(): array
{
return [
'r1' => new Route('/api/bar/action1'),
'r2' => new Route('/api/foo/action1'),
'r3' => new Route('/api/foo/action2'),
'r4' => new Route('/api/demo'),
'r5' => new Route('/_profiler/test/test'),
'r6' => new Route('/admin/bar/action1', [], [], [], 'www.example.com'),
'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'),
];
}
/**
* @dataProvider getMatchingRoutes
*/
public function testMatchingRoutes(string $name, Route $route, array $options = [])
{
$routes = new RouteCollection();
$routes->add($name, $route);
$routeBuilder = new FilteredRouteCollectionBuilder($options);
$filteredRoutes = $routeBuilder->filter($routes);
$this->assertCount(1, $filteredRoutes);
}
public function getMatchingRoutes(): array
{
return [
['r1', new Route('/api/bar/action1')],
['r2', new Route('/api/foo/action1'), ['path_patterns' => ['^/api', 'i/fo', 'n1$']]],
['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']]],
];
}
/**
* @dataProvider getNonMatchingRoutes
*/
public function testNonMatchingRoutes(string $name, Route $route, array $options = [])
{
$routes = new RouteCollection();
$routes->add($name, $route);
$routeBuilder = new FilteredRouteCollectionBuilder($options);
$filteredRoutes = $routeBuilder->filter($routes);
$this->assertCount(0, $filteredRoutes);
}
public function getNonMatchingRoutes(): array
{
return [
['r1', new Route('/api/bar/action1'), ['path_patterns' => ['^/apis']]],
['r2', new Route('/api/foo/action1'), ['path_patterns' => ['^/apis', 'i/foo/b', 'n1/$']]],
['r3_matching_path_and_non_matching_host', new Route('/api/foo/action2'), ['path_patterns' => ['^/api/foo/action2$'], 'host_patterns' => ['^api\.']]],
['r4_matching_path_and_non_matching_host', new Route('/api/bar/action1', [], [], [], 'www.example.com'), ['path_patterns' => ['^/api/'], 'host_patterns' => ['^api\.']]],
['r5_non_matching_path_and_matching_host', new Route('/admin/bar/action1', [], [], [], 'api.example.com'), ['path_patterns' => ['^/api/'], 'host_patterns' => ['^api\.']]],
['r6_non_matching_path_and_non_matching_host', new Route('/admin/bar/action1', [], [], [], 'www.example.com'), ['path_patterns' => ['^/api/'], 'host_patterns' => ['^api\.ex']]],
];
}
}

View File

@ -17,6 +17,7 @@
"require": {
"php": "^7.0",
"symfony/framework-bundle": "^3.4|^4.0",
"symfony/options-resolver": "^3.4.4|^4.0",
"symfony/property-info": "^3.4|^4.0",
"exsyst/swagger": "~0.3|~0.4",
"zircote/swagger-php": "^2.0.9",