mirror of
https://github.com/retailcrm/NelmioApiDocBundle.git
synced 2025-02-02 23:59:26 +03:00
Merge pull request #1257 from phansys/ticket_1121
Allow to filter routes by host (#1121)
This commit is contained in:
commit
3698daa547
10
CHANGELOG.md
10
CHANGELOG.md
@ -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)
|
||||
------------------
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
|
@ -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:
|
||||
|
@ -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?
|
||||
--------------------------
|
||||
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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
|
||||
{
|
||||
/**
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -13,6 +13,9 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
|
||||
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
|
||||
/**
|
||||
* @Route(host="api.example.com")
|
||||
*/
|
||||
class UndocumentedController
|
||||
{
|
||||
/**
|
||||
|
@ -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']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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\.']],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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']]],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user