diff --git a/Annotation/Operation.php b/Annotation/Operation.php
new file mode 100644
index 0000000..10f581a
--- /dev/null
+++ b/Annotation/Operation.php
@@ -0,0 +1,21 @@
+load('services.xml');
// Filter routes
- $routeCollectionBuilder = $container->getDefinition('nelmio_api_doc.describers.route.filtered_route_collection_builder');
+ $routeCollectionBuilder = $container->getDefinition('nelmio_api_doc.filtered_route_collection_builder');
$routeCollectionBuilder->replaceArgument(0, $config['routes']['path_patterns']);
// Import services needed for each library
diff --git a/Describer/SwaggerPhpDescriber.php b/Describer/SwaggerPhpDescriber.php
index d016b94..92aa455 100644
--- a/Describer/SwaggerPhpDescriber.php
+++ b/Describer/SwaggerPhpDescriber.php
@@ -11,13 +11,15 @@
namespace Nelmio\ApiDocBundle\Describer;
+use Doctrine\Common\Annotations\Reader;
+use EXSyst\Component\Swagger\Swagger;
+use Nelmio\ApiDocBundle\Annotation\Operation;
use Nelmio\ApiDocBundle\SwaggerPhp\AddDefaults;
use Nelmio\ApiDocBundle\SwaggerPhp\ModelRegister;
-use Nelmio\ApiDocBundle\SwaggerPhp\OperationResolver;
use Nelmio\ApiDocBundle\Util\ControllerReflector;
-use Swagger\Analyser;
use Swagger\Analysis;
-use Symfony\Component\Finder\Finder;
+use Swagger\Annotations as SWG;
+use Swagger\Context;
use Symfony\Component\Routing\RouteCollection;
final class SwaggerPhpDescriber extends ExternalDocDescriber implements ModelRegistryAwareInterface
@@ -26,57 +28,138 @@ final class SwaggerPhpDescriber extends ExternalDocDescriber implements ModelReg
private $routeCollection;
private $controllerReflector;
+ private $annotationReader;
- public function __construct(RouteCollection $routeCollection, ControllerReflector $controllerReflector, bool $overwrite = false)
+ public function __construct(RouteCollection $routeCollection, ControllerReflector $controllerReflector, Reader $annotationReader, bool $overwrite = false)
{
$this->routeCollection = $routeCollection;
$this->controllerReflector = $controllerReflector;
+ $this->annotationReader = $annotationReader;
parent::__construct(function () {
- $whitelist = Analyser::$whitelist;
- Analyser::$whitelist = false;
- try {
- $options = ['processors' => $this->getProcessors()];
- $annotation = \Swagger\scan($this->getFinder(), $options);
+ $analysis = $this->getAnnotations();
- return json_decode(json_encode($annotation));
- } finally {
- Analyser::$whitelist = $whitelist;
- }
+ $analysis->process($this->getProcessors());
+ $analysis->validate();
+
+ return json_decode(json_encode($analysis->swagger));
}, $overwrite);
}
- private function getFinder()
- {
- $files = [];
- foreach ($this->routeCollection->all() as $route) {
- if (!$route->hasDefault('_controller')) {
- continue;
- }
-
- // if able to resolve the controller
- $controller = $route->getDefault('_controller');
- if ($callable = $this->controllerReflector->getReflectionClassAndMethod($controller)) {
- list($class, $method) = $callable;
-
- $files[$class->getFileName()] = true;
- }
- }
-
- $finder = new Finder();
- $finder->append(array_keys($files));
-
- return $finder;
- }
-
private function getProcessors(): array
{
$processors = [
new AddDefaults(),
new ModelRegister($this->modelRegistry),
- new OperationResolver($this->routeCollection, $this->controllerReflector),
];
return array_merge($processors, Analysis::processors());
}
+
+ private function getAnnotations(): Analysis
+ {
+ $analysis = new Analysis();
+
+ $operationAnnotations = [
+ 'get' => SWG\Get::class,
+ 'post' => SWG\Post::class,
+ 'put' => SWG\Put::class,
+ 'patch' => SWG\Patch::class,
+ 'delete' => SWG\Delete::class,
+ 'options' => SWG\Options::class,
+ 'head' => SWG\Head::class,
+ ];
+
+ foreach ($this->getMethodsToParse() as $method => list($path, $httpMethods)) {
+ $annotations = array_filter($this->annotationReader->getMethodAnnotations($method), function ($v) {
+ return $v instanceof SWG\AbstractAnnotation;
+ });
+
+ if (0 === count($annotations)) {
+ continue;
+ }
+
+ $declaringClass = $method->getDeclaringClass();
+ $context = new Context([
+ 'namespace' => $method->getNamespaceName(),
+ 'class' => $declaringClass->getShortName(),
+ 'method' => $method->name,
+ 'filename' => $method->getFileName(),
+ ]);
+ $implicitAnnotations = [];
+ foreach ($annotations as $annotation) {
+ $annotation->_context = $context;
+
+ if ($annotation instanceof Operation) {
+ foreach ($httpMethods as $httpMethod) {
+ $annotationClass = $operationAnnotations[$httpMethod];
+ $operation = new $annotationClass(['_context' => $context]);
+ $operation->path = $path;
+ $operation->mergeProperties($annotation);
+
+ $analysis->addAnnotation($operation, null);
+ }
+
+ continue;
+ }
+
+ if ($annotation instanceof SWG\Operation) {
+ if (null === $annotation->path) {
+ $annotation = clone $annotation;
+ $annotation->path = $path;
+ }
+
+ $analysis->addAnnotation($annotation, null);
+
+ continue;
+ }
+
+ if (!$annotation instanceof SWG\Response && !$annotation instanceof SWG\Parameter && !$annotation instanceof SWG\ExternalDocumentation) {
+ throw new \LogicException(sprintf('Using the annotation "%s" as a root annotation in "%s::%s()" is not allowed.', get_class($annotation), $method->getDeclaringClass()->name, $method->name));
+ }
+
+ $implicitAnnotations[] = $annotation;
+ }
+
+ if (0 === count($implicitAnnotations)) {
+ continue;
+ }
+
+ foreach ($httpMethods as $httpMethod) {
+ $annotationClass = $operationAnnotations[$httpMethod];
+ $operation = new $annotationClass(['_context' => $context, 'path' => $path, 'value' => $implicitAnnotations]);
+ $analysis->addAnnotation($operation, null);
+ }
+ }
+
+ return $analysis;
+ }
+
+ private function getMethodsToParse()
+ {
+ foreach ($this->routeCollection->all() as $route) {
+ if (!$route->hasDefault('_controller')) {
+ continue;
+ }
+
+ $controller = $route->getDefault('_controller');
+ if ($callable = $this->controllerReflector->getReflectionClassAndMethod($controller)) {
+ list($class, $method) = $callable;
+ $path = $this->normalizePath($route->getPath());
+ $httpMethods = $route->getMethods() ?: Swagger::$METHODS;
+ $httpMethods = array_map('strtolower', $httpMethods);
+
+ yield $method => [$path, $httpMethods];
+ }
+ }
+ }
+
+ private function normalizePath(string $path)
+ {
+ if (substr($path, -10) === '.{_format}') {
+ $path = substr($path, 0, -10);
+ }
+
+ return $path;
+ }
}
diff --git a/Resources/config/services.xml b/Resources/config/services.xml
index 8d72d7a..583f6dd 100644
--- a/Resources/config/services.xml
+++ b/Resources/config/services.xml
@@ -14,15 +14,8 @@
-
-
-
-
-
-
-
-
-
+
+
@@ -30,12 +23,19 @@
-
+
+
+
+
+
+
+
+
-
+
diff --git a/Resources/config/swagger_php.xml b/Resources/config/swagger_php.xml
index 70bf245..bb4855e 100644
--- a/Resources/config/swagger_php.xml
+++ b/Resources/config/swagger_php.xml
@@ -5,12 +5,9 @@
-
-
-
-
-
+
+
diff --git a/SwaggerPhp/ModelRegister.php b/SwaggerPhp/ModelRegister.php
index 84a203c..5fe105e 100644
--- a/SwaggerPhp/ModelRegister.php
+++ b/SwaggerPhp/ModelRegister.php
@@ -38,38 +38,51 @@ final class ModelRegister
public function __invoke(Analysis $analysis)
{
foreach ($analysis->annotations as $annotation) {
- if (!$annotation instanceof ModelAnnotation || $annotation->_context->not('nested')) {
- continue;
- }
-
- if (!is_string($annotation->type)) {
- // Ignore invalid annotations, they are validated later
- continue;
- }
-
- $parent = $annotation->_context->nested;
- if (!$parent instanceof Response && !$parent instanceof Parameter && !$parent instanceof Schema) {
- continue;
- }
-
- $annotationClass = Schema::class;
- if ($parent instanceof Schema) {
+ if ($annotation instanceof Response) {
+ $annotationClass = Schema::class;
+ } elseif ($annotation instanceof Parameter) {
+ if ('array' === $annotation->type) {
+ $annotationClass = Items::class;
+ } else {
+ $annotationClass = Schema::class;
+ }
+ } elseif ($annotation instanceof Schema) {
$annotationClass = Items::class;
+ } else {
+ continue;
}
- $parent->merge([new $annotationClass([
- 'ref' => $this->modelRegistry->register(new Model($this->createType($annotation->type))),
- ])]);
-
- // It is no longer an unmerged annotation
- foreach ($parent->_unmerged as $key => $unmerged) {
- if ($unmerged === $annotation) {
- unset($parent->_unmerged[$key]);
+ $model = null;
+ foreach ($annotation->_unmerged as $unmerged) {
+ if ($unmerged instanceof ModelAnnotation) {
+ $model = $unmerged;
break;
}
}
- $analysis->annotations->detach($annotation);
+
+ if (null === $model || !$model instanceof ModelAnnotation) {
+ continue;
+ }
+
+ if (!is_string($model->type)) {
+ // Ignore invalid annotations, they are validated later
+ continue;
+ }
+
+ $annotation->merge([new $annotationClass([
+ 'ref' => $this->modelRegistry->register(new Model($this->createType($model->type))),
+ ])]);
+
+ // It is no longer an unmerged annotation
+ foreach ($annotation->_unmerged as $key => $unmerged) {
+ if ($unmerged === $model) {
+ unset($annotation->_unmerged[$key]);
+
+ break;
+ }
+ }
+ $analysis->annotations->detach($model);
}
}
diff --git a/SwaggerPhp/OperationResolver.php b/SwaggerPhp/OperationResolver.php
deleted file mode 100644
index f7220c5..0000000
--- a/SwaggerPhp/OperationResolver.php
+++ /dev/null
@@ -1,195 +0,0 @@
-routeCollection = $routeCollection;
- $this->controllerReflector = $controllerReflector;
- }
-
- public function __invoke(Analysis $analysis)
- {
- $this->resolveOperationsPath($analysis);
- $this->createImplicitOperations($analysis);
- }
-
- private function resolveOperationsPath(Analysis $analysis)
- {
- $operations = $analysis->getAnnotationsOfType(SWG\Operation::class);
- foreach ($operations as $operation) {
- if (null !== $operation->path || $operation->_context->not('method')) {
- continue;
- }
-
- $paths = $this->getPaths($operation->_context, $operation->method);
- if (0 === count($paths)) {
- continue;
- }
-
- // Define the path of the first annotation
- $operation->path = array_pop($paths);
-
- // If there are other paths, clone the annotation
- foreach ($paths as $path) {
- $alias = clone $operation;
- $alias->path = $path;
-
- $analysis->addAnnotation($alias, $alias->_context);
- }
- }
- }
-
- private function createImplicitOperations(Analysis $analysis)
- {
- $annotations = array_merge($analysis->getAnnotationsOfType(SWG\Response::class), $analysis->getAnnotationsOfType(SWG\Parameter::class), $analysis->getAnnotationsOfType(SWG\ExternalDocumentation::class));
- $map = [];
- foreach ($annotations as $annotation) {
- $context = $annotation->_context;
- if ($context->not('method')) {
- continue;
- }
-
- $class = $this->getClass($context);
- $method = $context->method;
-
- $id = $class.'|'.$method;
- if (!isset($map[$id])) {
- $map[$id] = [];
- }
-
- $map[$id][] = $annotation;
- }
-
- $operationAnnotations = [
- 'get' => SWG\Get::class,
- 'post' => SWG\Post::class,
- 'put' => SWG\Put::class,
- 'patch' => SWG\Patch::class,
- 'delete' => SWG\Delete::class,
- 'options' => SWG\Options::class,
- 'head' => SWG\Head::class,
- ];
- foreach ($map as $id => $annotations) {
- $context = $annotations[0]->_context;
- $httpMethods = $this->getHttpMethods($context);
- foreach ($httpMethods as $httpMethod => $paths) {
- $annotationClass = $operationAnnotations[$httpMethod];
- foreach ($paths as $path => $v) {
- $operation = new $annotationClass(['path' => $path, 'value' => $annotations], $context);
- $analysis->addAnnotation($operation, $context);
- }
- }
-
- foreach ($annotations as $annotation) {
- $analysis->annotations->detach($annotation);
- }
- }
- }
-
- private function getPaths(Context $context, string $httpMethod): array
- {
- $httpMethods = $this->getHttpMethods($context);
- if (!isset($httpMethods[$httpMethod])) {
- return [];
- }
-
- return array_keys($httpMethods[$httpMethod]);
- }
-
- private function getHttpMethods(Context $context)
- {
- if (null === $this->controllerMap) {
- $this->buildMap();
- }
-
- $class = $this->getClass($context);
- $method = $context->method;
-
- // Checks if a route corresponds to this method
- if (!isset($this->controllerMap[$class][$method])) {
- return [];
- }
-
- return $this->controllerMap[$class][$method];
- }
-
- private function getClass(Context $context)
- {
- return ltrim($context->namespace.'\\'.$context->class, '\\');
- }
-
- private function buildMap()
- {
- $this->controllerMap = [];
- foreach ($this->routeCollection->all() as $route) {
- if (!$route->hasDefault('_controller')) {
- continue;
- }
-
- $controller = $route->getDefault('_controller');
- if ($callable = $this->controllerReflector->getReflectionClassAndMethod($controller)) {
- list($class, $method) = $callable;
- $class = $class->name;
- $method = $method->name;
-
- if (!isset($this->controllerMap[$class])) {
- $this->controllerMap[$class] = [];
- }
- if (!isset($this->controllerMap[$class][$method])) {
- $this->controllerMap[$class][$method] = [];
- }
-
- $httpMethods = $route->getMethods() ?: Swagger::$METHODS;
- foreach ($httpMethods as $httpMethod) {
- $httpMethod = strtolower($httpMethod);
- if (!isset($this->controllerMap[$class][$method][$httpMethod])) {
- $this->controllerMap[$class][$method][$httpMethod] = [];
- }
-
- $path = $this->normalizePath($route->getPath());
- $this->controllerMap[$class][$method][$httpMethod][$path] = true;
- }
- }
- }
- }
-
- private function normalizePath(string $path)
- {
- if (substr($path, -10) === '.{_format}') {
- $path = substr($path, 0, -10);
- }
-
- return $path;
- }
-}
diff --git a/Tests/Functional/Controller/ApiController.php b/Tests/Functional/Controller/ApiController.php
index 3fa6f75..c182ba5 100644
--- a/Tests/Functional/Controller/ApiController.php
+++ b/Tests/Functional/Controller/ApiController.php
@@ -14,6 +14,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Controller\Annotations\RequestParam;
use Nelmio\ApiDocBundle\Annotation\Model;
+use Nelmio\ApiDocBundle\Annotation\Operation;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\User;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Swagger\Annotations as SWG;
@@ -26,7 +27,7 @@ class ApiController
/**
* @Route("/swagger", methods={"GET"})
* @Route("/swagger2", methods={"GET"})
- * @SWG\Get(
+ * @Operation(
* @SWG\Response(response="201", description="An example resource")
* )
*/
@@ -92,4 +93,14 @@ class ApiController
public function adminAction()
{
}
+
+ /**
+ * @SWG\Get(
+ * path="/filtered",
+ * @SWG\Response(response="201", description="")
+ * )
+ */
+ public function filteredAction()
+ {
+ }
}
diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php
index 69272e7..ee3e26b 100644
--- a/Tests/Functional/FunctionalTest.php
+++ b/Tests/Functional/FunctionalTest.php
@@ -29,6 +29,13 @@ class FunctionalTest extends WebTestCase
$this->assertFalse($paths->has('/api/admin'));
}
+ public function testFilteredAction()
+ {
+ $paths = $this->getSwaggerDefinition()->getPaths();
+
+ $this->assertFalse($paths->has('/filtered'));
+ }
+
/**
* Tests that the paths are automatically resolved in Swagger annotations.
*