Merge pull request #943 from nelmio/SWAGGERMODEL

[3.0] Add a @Model annotation
This commit is contained in:
Guilhem N 2017-01-18 16:04:30 +01:00 committed by GitHub
commit 3a29c598cc
28 changed files with 793 additions and 49 deletions

29
Annotation/Model.php Normal file
View File

@ -0,0 +1,29 @@
<?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;
use Swagger\Annotations\AbstractAnnotation;
/**
* @Annotation
*/
final class Model extends AbstractAnnotation
{
public static $_required = ['type'];
public static $_parents = [
'Swagger\Annotations\Parameter',
'Swagger\Annotations\Response',
'Swagger\Annotations\Swagger',
];
public $type;
}

View File

@ -13,20 +13,26 @@ namespace Nelmio\ApiDocBundle;
use EXSyst\Component\Swagger\Swagger; use EXSyst\Component\Swagger\Swagger;
use Nelmio\ApiDocBundle\Describer\DescriberInterface; use Nelmio\ApiDocBundle\Describer\DescriberInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemPoolInterface;
final class ApiDocGenerator final class ApiDocGenerator
{ {
private $swagger; private $swagger;
private $describers; private $describers;
private $modelDescribers;
private $cacheItemPool; private $cacheItemPool;
/** /**
* @param DescriberInterface[] $describers * @param DescriberInterface[] $describers
* @param ModelDescriberInterface[] $modelDescribers
*/ */
public function __construct(array $describers, CacheItemPoolInterface $cacheItemPool = null) public function __construct(array $describers, array $modelDescribers, CacheItemPoolInterface $cacheItemPool = null)
{ {
$this->describers = $describers; $this->describers = $describers;
$this->modelDescribers = $modelDescribers;
$this->cacheItemPool = $cacheItemPool; $this->cacheItemPool = $cacheItemPool;
} }
@ -44,9 +50,15 @@ final class ApiDocGenerator
} }
$this->swagger = new Swagger(); $this->swagger = new Swagger();
$modelRegistry = new ModelRegistry($this->modelDescribers, $this->swagger);
foreach ($this->describers as $describer) { foreach ($this->describers as $describer) {
if ($describer instanceof ModelRegistryAwareInterface) {
$describer->setModelRegistry($modelRegistry);
}
$describer->describe($this->swagger); $describer->describe($this->swagger);
} }
$modelRegistry->registerDefinitions();
if (isset($item)) { if (isset($item)) {
$this->cacheItemPool->save($item->set($this->swagger)); $this->cacheItemPool->save($item->set($this->swagger));

View File

@ -0,0 +1,30 @@
<?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\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* @internal
*/
final class AddModelDescribersPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;
public function process(ContainerBuilder $container)
{
$modelDescribers = $this->findAndSortTaggedServices('nelmio_api_doc.model_describer', $container);
$container->getDefinition('nelmio_api_doc.generator')->replaceArgument(1, $modelDescribers);
}
}

View File

@ -38,7 +38,7 @@ final class DefaultDescriber implements DescriberInterface
$operation = $path->getOperation($method); $operation = $path->getOperation($method);
// Default Response // Default Response
if (0 === iterator_count($operation->getResponses())) { if (0 === count($operation->getResponses())) {
$defaultResponse = $operation->getResponses()->get('default'); $defaultResponse = $operation->getResponses()->get('default');
$defaultResponse->setDescription(''); $defaultResponse->setDescription('');
} }

View File

@ -0,0 +1,19 @@
<?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\Describer;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
interface ModelRegistryAwareInterface
{
public function setModelRegistry(ModelRegistry $modelRegistry);
}

View File

@ -0,0 +1,24 @@
<?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\Describer;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
trait ModelRegistryAwareTrait
{
private $modelRegistry;
public function setModelRegistry(ModelRegistry $modelRegistry)
{
$this->modelRegistry = $modelRegistry;
}
}

View File

@ -17,8 +17,10 @@ use Nelmio\ApiDocBundle\Util\ControllerReflector;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\RouteCollection;
final class RouteDescriber implements DescriberInterface final class RouteDescriber implements DescriberInterface, ModelRegistryAwareInterface
{ {
use ModelRegistryAwareTrait;
private $routeCollection; private $routeCollection;
private $controllerReflector; private $controllerReflector;
private $routeDescribers; private $routeDescribers;
@ -51,6 +53,10 @@ final class RouteDescriber implements DescriberInterface
if ($method = $this->controllerReflector->getReflectionMethod($controller)) { if ($method = $this->controllerReflector->getReflectionMethod($controller)) {
// Extract as many informations as possible about this route // Extract as many informations as possible about this route
foreach ($this->routeDescribers as $describer) { foreach ($this->routeDescribers as $describer) {
if ($describer instanceof ModelRegistryAwareInterface) {
$describer->setModelRegistry($this->modelRegistry);
}
$describer->describe($api, $route, $method); $describer->describe($api, $route, $method);
} }
} }

View File

@ -11,37 +11,30 @@
namespace Nelmio\ApiDocBundle\Describer; namespace Nelmio\ApiDocBundle\Describer;
use Nelmio\ApiDocBundle\SwaggerPhp\AddDefaults;
use Nelmio\ApiDocBundle\SwaggerPhp\ModelRegister;
use Nelmio\ApiDocBundle\SwaggerPhp\OperationResolver; use Nelmio\ApiDocBundle\SwaggerPhp\OperationResolver;
use Swagger\Analyser;
use Swagger\Analysis; use Swagger\Analysis;
final class SwaggerPhpDescriber extends ExternalDocDescriber final class SwaggerPhpDescriber extends ExternalDocDescriber implements ModelRegistryAwareInterface
{ {
use ModelRegistryAwareTrait;
private $operationResolver; private $operationResolver;
public function __construct(string $projectPath, bool $overwrite = false) public function __construct(string $projectPath, bool $overwrite = false)
{ {
$nelmioNamespace = 'Nelmio\\ApiDocBundle\\';
if (!in_array($nelmioNamespace, Analyser::$whitelist)) {
Analyser::$whitelist[] = $nelmioNamespace;
}
parent::__construct(function () use ($projectPath) { parent::__construct(function () use ($projectPath) {
// Ignore notices as the documentation can be completed by other describers $options = ['processors' => $this->getProcessors()];
$prevHandler = set_error_handler(function ($type, $message, $file, $line, $context) use (&$prevHandler) { $annotation = \Swagger\scan($projectPath, $options);
if (E_USER_NOTICE === $type || E_USER_WARNING === $type) {
return;
}
return null !== $prevHandler && call_user_func($prevHandler, $type, $message, $file, $line, $context); return json_decode(json_encode($annotation));
});
try {
$options = [];
if (null !== $this->operationResolver) {
$options['processors'] = array_merge([$this->operationResolver], Analysis::processors());
}
$annotation = \Swagger\scan($projectPath, $options);
return json_decode(json_encode($annotation));
} finally {
restore_error_handler();
}
}, $overwrite); }, $overwrite);
} }
@ -53,4 +46,17 @@ final class SwaggerPhpDescriber extends ExternalDocDescriber
{ {
$this->operationResolver = $operationResolver; $this->operationResolver = $operationResolver;
} }
private function getProcessors(): array
{
$processors = [
new AddDefaults(),
new ModelRegister($this->modelRegistry),
];
if (null !== $this->operationResolver) {
$processors[] = $this->operationResolver;
}
return array_merge($processors, Analysis::processors());
}
} }

45
EXSystApiDocBundle.php Normal file
View File

@ -0,0 +1,45 @@
<?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;
use Nelmio\ApiDocBundle\DependencyInjection\Compiler\AddDescribersPass;
use Nelmio\ApiDocBundle\DependencyInjection\Compiler\AddModelDescribersPass;
use Nelmio\ApiDocBundle\DependencyInjection\Compiler\AddRouteDescribersPass;
use Nelmio\ApiDocBundle\DependencyInjection\EXSystApiDocExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
final class EXSystApiDocBundle extends Bundle
{
/**
* {@inheritdoc}
*/
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new AddDescribersPass());
$container->addCompilerPass(new AddRouteDescribersPass());
$container->addCompilerPass(new AddModelDescribersPass());
}
/**
* {@inheritdoc}
*/
public function getContainerExtension()
{
if (null === $this->extension) {
$this->extension = new EXSystApiDocExtension();
}
if ($this->extension) {
return $this->extension;
}
}
}

37
Model/Model.php Normal file
View File

@ -0,0 +1,37 @@
<?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\Model;
use Symfony\Component\PropertyInfo\Type;
final class Model
{
private $type;
public function __construct(Type $type)
{
$this->type = $type;
}
/**
* @return Type|null
*/
public function getType()
{
return $this->type;
}
public function getHash()
{
return md5(serialize($this->type));
}
}

132
Model/ModelRegistry.php Normal file
View File

@ -0,0 +1,132 @@
<?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\Model;
use EXSyst\Component\Swagger\Schema;
use EXSyst\Component\Swagger\Swagger;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
use Symfony\Component\PropertyInfo\Type;
final class ModelRegistry
{
private $unregistered = [];
private $models = [];
private $names = [];
private $modelDescribers = [];
private $api;
/**
* @param ModelDescriberInterface[] $modelDescribers
*
* @internal
*/
public function __construct(array $modelDescribers, Swagger $api)
{
$this->modelDescribers = $modelDescribers;
$this->api = $api;
}
public function register(Model $model): string
{
$hash = $model->getHash();
if (isset($this->names[$hash])) {
return '#/definitions/'.$this->names[$hash];
}
$this->names[$hash] = $name = $this->generateModelName($model);
$this->models[$hash] = $model;
$this->unregistered[] = $hash;
// Reserve the name
$this->api->getDefinitions()->get($name);
return '#/definitions/'.$name;
}
/**
* @internal
*/
public function registerDefinitions()
{
while (count($this->unregistered)) {
$tmp = [];
foreach ($this->unregistered as $hash) {
$tmp[$this->names[$hash]] = $this->models[$hash];
}
$this->unregistered = [];
foreach ($tmp as $name => $model) {
$schema = null;
foreach ($this->modelDescribers as $modelDescriber) {
if ($modelDescriber instanceof ModelRegistryAwareInterface) {
$modelDescriber->setModelRegistry($this);
}
if ($modelDescriber->supports($model)) {
$schema = new Schema();
$modelDescriber->describe($model, $schema);
break;
}
}
if (null === $schema) {
throw new \LogicException(sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $this->typeToString($model->getType())));
}
$this->api->getDefinitions()->set($name, $schema);
}
}
}
private function generateModelName(Model $model): string
{
$definitions = $this->api->getDefinitions();
$base = $name = $this->getTypeShortName($model->getType());
$i = 1;
while ($definitions->has($name)) {
++$i;
$name = $base.$i;
}
return $name;
}
private function getTypeShortName(Type $type)
{
if (null !== $type->getCollectionValueType()) {
return $this->getTypeShortName($type->getCollectionValueType()).'[]';
}
if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType()) {
$parts = explode('\\', $type->getClassName());
return end($parts);
}
return $type->getBuiltinType();
}
private function typeToString(Type $type): string
{
if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType()) {
return $type->getClassName();
} elseif ($type->isCollection()) {
if (null !== $type->getCollectionValueType()) {
return $this->typeToString($type->getCollectionValueType()).'[]';
} else {
return 'mixed[]';
}
} else {
return $type->getBuiltinType();
}
}
}

View File

@ -0,0 +1,35 @@
<?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\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Model\Model;
class CollectionModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
{
use ModelRegistryAwareTrait;
public function describe(Model $model, Schema $schema)
{
$schema->setType('array');
$schema->getItems()->setRef(
$this->modelRegistry->register(new Model($model->getType()->getCollectionValueType()))
);
}
public function supports(Model $model)
{
return $model->getType()->isCollection() && null !== $model->getType()->getCollectionValueType();
}
}

View File

@ -0,0 +1,22 @@
<?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\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Model\Model;
interface ModelDescriberInterface
{
public function describe(Model $model, Schema $schema);
public function supports(Model $model);
}

View File

@ -0,0 +1,55 @@
<?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\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Model\Model;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
{
use ModelRegistryAwareTrait;
public function __construct(PropertyInfoExtractorInterface $propertyInfo)
{
$this->propertyInfo = $propertyInfo;
}
public function describe(Model $model, Schema $schema)
{
$schema->setType('object');
$properties = $schema->getProperties();
$class = $model->getType()->getClassName();
foreach ($this->propertyInfo->getProperties($class) as $propertyName) {
$types = $this->propertyInfo->getTypes($class, $propertyName);
if (0 === count($types)) {
throw new \LogicException(sprintf('The PropertyInfo component was not able to guess the type of %s::$%s', $class, $propertyName));
}
if (count($types) > 1) {
throw new \LogicException(sprintf('Property %s::$%s defines more than one type.', $class, $propertyName));
}
$properties->get($propertyName)->setRef(
$this->modelRegistry->register(new Model($types[0]))
);
}
}
public function supports(Model $model)
{
return Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType();
}
}

View File

@ -0,0 +1,36 @@
<?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\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Model\Model;
use Symfony\Component\PropertyInfo\Type;
class ScalarModelDescriber implements ModelDescriberInterface
{
private static $supportedTypes = [
Type::BUILTIN_TYPE_INT => 'integer',
Type::BUILTIN_TYPE_FLOAT => 'float',
Type::BUILTIN_TYPE_STRING => 'string',
];
public function describe(Model $model, Schema $schema)
{
$type = self::$supportedTypes[$model->getType()->getBuiltinType()];
$schema->setType($type);
}
public function supports(Model $model)
{
return isset(self::$supportedTypes[$model->getType()->getBuiltinType()]);
}
}

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle; namespace Nelmio\ApiDocBundle;
use Nelmio\ApiDocBundle\DependencyInjection\Compiler\AddDescribersPass; use Nelmio\ApiDocBundle\DependencyInjection\Compiler\AddDescribersPass;
use Nelmio\ApiDocBundle\DependencyInjection\Compiler\AddModelDescribersPass;
use Nelmio\ApiDocBundle\DependencyInjection\Compiler\AddRouteDescribersPass; use Nelmio\ApiDocBundle\DependencyInjection\Compiler\AddRouteDescribersPass;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
@ -24,6 +25,7 @@ final class NelmioApiDocBundle extends Bundle
public function build(ContainerBuilder $container) public function build(ContainerBuilder $container)
{ {
$container->addCompilerPass(new AddDescribersPass()); $container->addCompilerPass(new AddDescribersPass());
$container->addCompilerPass(new AddModelDescribersPass());
$container->addCompilerPass(new AddRouteDescribersPass()); $container->addCompilerPass(new AddRouteDescribersPass());
} }
} }

View File

@ -8,7 +8,7 @@
<argument type="service" id="nelmio_api_doc.describers.api_platform.documentation" /> <argument type="service" id="nelmio_api_doc.describers.api_platform.documentation" />
<argument type="service" id="api_platform.swagger.normalizer.documentation" /> <argument type="service" id="api_platform.swagger.normalizer.documentation" />
<tag name="nelmio_api_doc.describer" priority="-200" /> <tag name="nelmio_api_doc.describer" priority="-100" />
</service> </service>
<service id="nelmio_api_doc.describers.api_platform.documentation" class="ApiPlatform\Core\Documentation\Documentation" public="false"> <service id="nelmio_api_doc.describers.api_platform.documentation" class="ApiPlatform\Core\Documentation\Documentation" public="false">

View File

@ -5,7 +5,8 @@
<services> <services>
<service id="nelmio_api_doc.generator" class="Nelmio\ApiDocBundle\ApiDocGenerator"> <service id="nelmio_api_doc.generator" class="Nelmio\ApiDocBundle\ApiDocGenerator">
<argument type="collection" /> <argument type="collection" /> <!-- Describers -->
<argument type="collection" /> <!-- Model Describers -->
</service> </service>
<service id="nelmio_api_doc.controller_reflector" class="Nelmio\ApiDocBundle\Util\ControllerReflector" public="false"> <service id="nelmio_api_doc.controller_reflector" class="Nelmio\ApiDocBundle\Util\ControllerReflector" public="false">
@ -18,7 +19,6 @@
<argument type="collection" /> <!-- Path patterns --> <argument type="collection" /> <!-- Path patterns -->
</service> </service>
<service id="nelmio_api_doc.describers.route" class="Nelmio\ApiDocBundle\Describer\RouteDescriber" public="false"> <service id="nelmio_api_doc.describers.route" class="Nelmio\ApiDocBundle\Describer\RouteDescriber" public="false">
<argument type="service"> <argument type="service">
<service class="Symfony\Component\Routing\RouteCollection"> <service class="Symfony\Component\Routing\RouteCollection">
@ -33,16 +33,31 @@
<argument type="service" id="nelmio_api_doc.controller_reflector" /> <argument type="service" id="nelmio_api_doc.controller_reflector" />
<argument type="collection" /> <argument type="collection" />
<tag name="nelmio_api_doc.describer" priority="-100" /> <tag name="nelmio_api_doc.describer" priority="-400" />
</service> </service>
<service id="nelmio_api_doc.describers.default" class="Nelmio\ApiDocBundle\Describer\DefaultDescriber" public="false"> <service id="nelmio_api_doc.describers.default" class="Nelmio\ApiDocBundle\Describer\DefaultDescriber" public="false">
<tag name="nelmio_api_doc.describer" priority="-1000" /> <tag name="nelmio_api_doc.describer" priority="-1000" />
</service> </service>
<!-- Routing Extractors --> <!-- Routing Describers -->
<service id="nelmio_api_doc.route_describers.route_metadata" class="Nelmio\ApiDocBundle\RouteDescriber\RouteMetadataDescriber" public="false"> <service id="nelmio_api_doc.route_describers.route_metadata" class="Nelmio\ApiDocBundle\RouteDescriber\RouteMetadataDescriber" public="false">
<tag name="nelmio_api_doc.route_describer" priority="-100" /> <tag name="nelmio_api_doc.route_describer" priority="-300" />
</service>
<!-- Model Describers -->
<service id="nelmio_api_doc.model_describers.object" class="Nelmio\ApiDocBundle\ModelDescriber\ObjectModelDescriber" public="false">
<argument type="service" id="property_info" />
<tag name="nelmio_api_doc.model_describer" />
</service>
<service id="nelmio_api_doc.model_describers.collection" class="Nelmio\ApiDocBundle\ModelDescriber\CollectionModelDescriber" public="false">
<tag name="nelmio_api_doc.model_describer" />
</service>
<service id="nelmio_api_doc.model_describers.scalar" class="Nelmio\ApiDocBundle\ModelDescriber\ScalarModelDescriber" public="false">
<tag name="nelmio_api_doc.model_describer" />
</service> </service>
</services> </services>

View File

@ -10,7 +10,16 @@
<argument type="service" id="nelmio_api_doc.describers.swagger_php.operation_resolver" /> <argument type="service" id="nelmio_api_doc.describers.swagger_php.operation_resolver" />
</call> </call>
<tag name="nelmio_api_doc.describer" priority="-300" /> <tag name="nelmio_api_doc.describer" priority="-200" />
</service>
<service id="nelmio_api_doc.describers.swagger_php.path_resolver" class="Nelmio\ApiDocBundle\SwaggerPhp\PathResolver" public="false">
<argument type="service">
<service class="Symfony\Component\Routing\RouteCollection">
<factory service="router" method="getRouteCollection" />
</service>
</argument>
<argument type="service" id="nelmio_api_doc.controller_reflector" />
</service> </service>
<service id="nelmio_api_doc.describers.swagger_php.operation_resolver" class="Nelmio\ApiDocBundle\SwaggerPhp\OperationResolver" public="false"> <service id="nelmio_api_doc.describers.swagger_php.operation_resolver" class="Nelmio\ApiDocBundle\SwaggerPhp\OperationResolver" public="false">

View File

@ -0,0 +1,37 @@
<?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\SwaggerPhp;
use Swagger\Analysis;
use Swagger\Annotations\Info;
use Swagger\Annotations\Swagger;
use Swagger\Context;
/**
* Add defaults to fix default warnings.
*
* @internal
*/
final class AddDefaults
{
public function __invoke(Analysis $analysis)
{
if ($analysis->getAnnotationsOfType(Info::class)) {
return;
}
if (($annotations = $analysis->getAnnotationsOfType(Swagger::class)) && null !== $annotations[0]->info) {
return;
}
$analysis->addAnnotation(new Info(['title' => '', 'version' => '0.0.0', '_context' => new Context(['generated' => true])]), null);
}
}

View File

@ -0,0 +1,84 @@
<?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\SwaggerPhp;
use Nelmio\ApiDocBundle\Annotation\Model as ModelAnnotation;
use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Swagger\Analysis;
use Swagger\Annotations\Items;
use Swagger\Annotations\Parameter;
use Swagger\Annotations\Response;
use Swagger\Annotations\Schema;
use Symfony\Component\PropertyInfo\Type;
/**
* Resolves the path in SwaggerPhp annotation when needed.
*
* @internal
*/
final class ModelRegister
{
private $modelRegistry;
public function __construct(ModelRegistry $modelRegistry)
{
$this->modelRegistry = $modelRegistry;
}
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) {
$annotationClass = Items::class;
}
$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]);
break;
}
}
$analysis->annotations->detach($annotation);
}
}
private function createType(string $type): Type
{
if ('[]' === substr($type, -2)) {
return new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, $this->createType(substr($type, 0, -2)));
}
return new Type(Type::BUILTIN_TYPE_OBJECT, false, $type);
}
}

View File

@ -11,7 +11,9 @@
namespace Nelmio\ApiDocBundle\Tests\Describer; namespace Nelmio\ApiDocBundle\Tests\Describer;
use EXSyst\Component\Swagger\Swagger;
use Nelmio\ApiDocBundle\Describer\SwaggerPhpDescriber; use Nelmio\ApiDocBundle\Describer\SwaggerPhpDescriber;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
class SwaggerPhpDescriberTest extends AbstractDescriberTest class SwaggerPhpDescriberTest extends AbstractDescriberTest
{ {
@ -27,5 +29,6 @@ class SwaggerPhpDescriberTest extends AbstractDescriberTest
protected function setUp() protected function setUp()
{ {
$this->describer = new SwaggerPhpDescriber(__DIR__.'/../Fixtures'); $this->describer = new SwaggerPhpDescriber(__DIR__.'/../Fixtures');
$this->describer->setModelRegistry(new ModelRegistry([], new Swagger()));
} }
} }

View File

@ -14,6 +14,9 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use FOS\RestBundle\Controller\Annotations\QueryParam; use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Controller\Annotations\RequestParam; use FOS\RestBundle\Controller\Annotations\RequestParam;
use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\Dummy;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\User;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Swagger\Annotations as SWG; use Swagger\Annotations as SWG;
@ -35,11 +38,19 @@ class ApiController
/** /**
* @Route("/swagger/implicit", methods={"GET", "POST"}) * @Route("/swagger/implicit", methods={"GET", "POST"})
* @SWG\Response(response="201", description="Operation automatically detected") * @SWG\Response(
* response="201",
* description="Operation automatically detected",
* @Model(type="Nelmio\ApiDocBundle\Tests\Functional\Entity\User")
* )
* @SWG\Parameter( * @SWG\Parameter(
* name="foo", * name="foo",
* in="query", * in="body",
* description="This is a parameter" * description="This is a parameter",
* @SWG\Schema(
* type="array",
* @Model(type="Nelmio\ApiDocBundle\Tests\Functional\Entity\User")
* )
* ) * )
*/ */
public function implicitSwaggerAction() public function implicitSwaggerAction()

View File

@ -42,11 +42,6 @@ class Dummy
*/ */
private $name; private $name;
/**
* @var array
*/
private $foo;
public function getId(): int public function getId(): int
{ {
return $this->id; return $this->id;
@ -65,8 +60,4 @@ class Dummy
public function hasRole(string $role) public function hasRole(string $role)
{ {
} }
public function setFoo(array $foo = null)
{
}
} }

View File

@ -0,0 +1,26 @@
<?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\Tests\Functional\Entity;
/**
* @author Guilhem N. <egetick@gmail.com>
*/
class User
{
public function addUsers(User $user)
{
}
public function setDummy(Dummy $dummy)
{
}
}

View File

@ -11,6 +11,8 @@
namespace Nelmio\ApiDocBundle\Tests\Functional; namespace Nelmio\ApiDocBundle\Tests\Functional;
use EXSyst\Component\Swagger\Operation;
use EXSyst\Component\Swagger\Schema;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class FunctionalTest extends WebTestCase class FunctionalTest extends WebTestCase
@ -50,11 +52,16 @@ class FunctionalTest extends WebTestCase
$responses = $operation->getResponses(); $responses = $operation->getResponses();
$this->assertTrue($responses->has('201')); $this->assertTrue($responses->has('201'));
$this->assertEquals('Operation automatically detected', $responses->get('201')->getDescription()); $response = $responses->get('201');
$this->assertEquals('Operation automatically detected', $response->getDescription());
$this->assertEquals('#/definitions/User', $response->getSchema()->getRef());
$parameters = $operation->getParameters(); $parameters = $operation->getParameters();
$this->assertTrue($parameters->has('foo', 'query')); $this->assertTrue($parameters->has('foo', 'body'));
$this->assertEquals('This is a parameter', $parameters->get('foo', 'query')->getDescription()); $parameter = $parameters->get('foo', 'body');
$this->assertEquals('This is a parameter', $parameter->getDescription());
$this->assertEquals('#/definitions/User', $parameter->getSchema()->getItems()->getRef());
} }
public function implicitSwaggerActionMethodsProvider() public function implicitSwaggerActionMethodsProvider()
@ -109,6 +116,27 @@ class FunctionalTest extends WebTestCase
$operation = $this->getOperation('/api/dummies/{id}', 'get'); $operation = $this->getOperation('/api/dummies/{id}', 'get');
} }
public function testUserModel()
{
$model = $this->getModel('User');
$this->assertEquals('object', $model->getType());
$properties = $model->getProperties();
$this->assertTrue($properties->has('users'));
$this->assertEquals('#/definitions/User[]', $properties->get('users')->getRef());
$this->assertTrue($properties->has('dummy'));
$this->assertEquals('#/definitions/Dummy2', $properties->get('dummy')->getRef());
}
public function testUsersModel()
{
$model = $this->getModel('User[]');
$this->assertEquals('array', $model->getType());
$this->assertEquals('#/definitions/User', $model->getItems()->getRef());
}
private function getSwaggerDefinition() private function getSwaggerDefinition()
{ {
static::createClient(); static::createClient();
@ -116,12 +144,20 @@ class FunctionalTest extends WebTestCase
return static::$kernel->getContainer()->get('nelmio_api_doc.generator')->generate(); return static::$kernel->getContainer()->get('nelmio_api_doc.generator')->generate();
} }
private function getOperation($path, $method) private function getModel($name): Schema
{
$definitions = $this->getSwaggerDefinition()->getDefinitions();
$this->assertTrue($definitions->has($name));
return $definitions->get($name);
}
private function getOperation($path, $method): Operation
{ {
$api = $this->getSwaggerDefinition(); $api = $this->getSwaggerDefinition();
$paths = $api->getPaths(); $paths = $api->getPaths();
$this->assertTrue($paths->has($path), sprintf('Path "%s" does not exist', $path)); $this->assertTrue($paths->has($path), sprintf('Path "%s" does not exist.', $path));
$action = $paths->get($path); $action = $paths->get($path);
$this->assertTrue($action->hasOperation($method), sprintf('Operation "%s" for path "%s" does not exist', $path, $method)); $this->assertTrue($action->hasOperation($method), sprintf('Operation "%s" for path "%s" does not exist', $path, $method));

View File

@ -0,0 +1,41 @@
<?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\Tests\Model;
use EXSyst\Component\Swagger\Schema;
use EXSyst\Component\Swagger\Swagger;
use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Symfony\Component\PropertyInfo\Type;
class ModelRegistryTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider unsupportedTypesProvider
*/
public function testUnsupportedTypeException(Type $type, string $stringType)
{
$this->setExpectedException('\LogicException', sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $stringType));
$registry = new ModelRegistry([], new Swagger());
$registry->register(new Model($type));
$registry->registerDefinitions();
}
public function unsupportedTypesProvider()
{
return [
[new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true), 'mixed[]'],
[new Type(Type::BUILTIN_TYPE_OBJECT, false, self::class), self::class],
];
}
}

View File

@ -17,6 +17,7 @@
"require": { "require": {
"php": "~7.0|~7.1", "php": "~7.0|~7.1",
"symfony/framework-bundle": "^2.8|^3.0", "symfony/framework-bundle": "^2.8|^3.0",
"symfony/property-info": "^2.8|^3.0",
"exsyst/swagger": "~0.2.3" "exsyst/swagger": "~0.2.3"
}, },
"require-dev": { "require-dev": {