Support `@Model` in in-object annotations

This commit is contained in:
Guilhem Niot 2018-03-17 19:23:29 +01:00
parent 61cda0161c
commit 8026ff46eb
10 changed files with 106 additions and 51 deletions

View File

@ -13,6 +13,7 @@ namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use EXSyst\Component\Swagger\Schema; use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
/** /**
* @internal * @internal
@ -20,17 +21,19 @@ use EXSyst\Component\Swagger\Schema;
class AnnotationsReader class AnnotationsReader
{ {
private $annotationsReader; private $annotationsReader;
private $modelRegistry;
private $phpDocReader; private $phpDocReader;
private $swgAnnotationsReader; private $swgAnnotationsReader;
private $symfonyConstraintAnnotationReader; private $symfonyConstraintAnnotationReader;
public function __construct(Reader $annotationsReader) public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry)
{ {
$this->annotationsReader = $annotationsReader; $this->annotationsReader = $annotationsReader;
$this->modelRegistry = $modelRegistry;
$this->phpDocReader = new PropertyPhpDocReader(); $this->phpDocReader = new PropertyPhpDocReader();
$this->swgAnnotationsReader = new SwgAnnotationsReader($annotationsReader); $this->swgAnnotationsReader = new SwgAnnotationsReader($annotationsReader, $modelRegistry);
$this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($annotationsReader); $this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($annotationsReader);
} }
@ -45,10 +48,10 @@ class AnnotationsReader
return $this->swgAnnotationsReader->getPropertyName($reflectionProperty, $default); return $this->swgAnnotationsReader->getPropertyName($reflectionProperty, $default);
} }
public function updateProperty(\ReflectionProperty $reflectionProperty, Schema $property) public function updateProperty(\ReflectionProperty $reflectionProperty, Schema $property, array $serializationGroups = null)
{ {
$this->phpDocReader->updateProperty($reflectionProperty, $property); $this->phpDocReader->updateProperty($reflectionProperty, $property);
$this->swgAnnotationsReader->updateProperty($reflectionProperty, $property); $this->swgAnnotationsReader->updateProperty($reflectionProperty, $property, $serializationGroups);
$this->symfonyConstraintAnnotationReader->updateProperty($reflectionProperty, $property); $this->symfonyConstraintAnnotationReader->updateProperty($reflectionProperty, $property);
} }
} }

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations; namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
use EXSyst\Component\Swagger\Schema; use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Model\Model;
use phpDocumentor\Reflection\DocBlock\Tags\Var_; use phpDocumentor\Reflection\DocBlock\Tags\Var_;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;

View File

@ -13,8 +13,13 @@ namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use EXSyst\Component\Swagger\Schema; use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Nelmio\ApiDocBundle\SwaggerPhp\ModelRegister;
use Swagger\Analysis;
use Swagger\Annotations\Definition as SwgDefinition; use Swagger\Annotations\Definition as SwgDefinition;
use Swagger\Annotations\Property as SwgProperty; use Swagger\Annotations\Property as SwgProperty;
use Swagger\Context;
/** /**
* @internal * @internal
@ -22,10 +27,12 @@ use Swagger\Annotations\Property as SwgProperty;
class SwgAnnotationsReader class SwgAnnotationsReader
{ {
private $annotationsReader; private $annotationsReader;
private $modelRegister;
public function __construct(Reader $annotationsReader) public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry)
{ {
$this->annotationsReader = $annotationsReader; $this->annotationsReader = $annotationsReader;
$this->modelRegister = new ModelRegister($modelRegistry);
} }
public function updateDefinition(\ReflectionClass $reflectionClass, Schema $schema) public function updateDefinition(\ReflectionClass $reflectionClass, Schema $schema)
@ -35,9 +42,14 @@ class SwgAnnotationsReader
return; return;
} }
if (null !== $swgDefinition->required) { // Read @Model annotations
$schema->setRequired($swgDefinition->required); $this->modelRegister->__invoke(new Analysis([$swgDefinition]));
if (!$swgDefinition->validate()) {
return;
} }
$schema->merge(json_decode(json_encode($swgDefinition)));
} }
public function getPropertyName(\ReflectionProperty $reflectionProperty, string $default): string public function getPropertyName(\ReflectionProperty $reflectionProperty, string $default): string
@ -50,13 +62,24 @@ class SwgAnnotationsReader
return $swgProperty->property ?? $default; return $swgProperty->property ?? $default;
} }
public function updateProperty(\ReflectionProperty $reflectionProperty, Schema $property) public function updateProperty(\ReflectionProperty $reflectionProperty, Schema $property, array $serializationGroups = null)
{ {
/** @var SwgProperty $swgProperty */
if (!$swgProperty = $this->annotationsReader->getPropertyAnnotation($reflectionProperty, SwgProperty::class)) { if (!$swgProperty = $this->annotationsReader->getPropertyAnnotation($reflectionProperty, SwgProperty::class)) {
return; return;
} }
$declaringClass = $reflectionProperty->getDeclaringClass();
$context = new Context([
'namespace' => $declaringClass->getNamespaceName(),
'class' => $declaringClass->getShortName(),
'property' => $reflectionProperty->name,
'filename' => $declaringClass->getFileName(),
]);
$swgProperty->_context = $context;
// Read @Model annotations
$this->modelRegister->__invoke(new Analysis([$swgProperty]), $serializationGroups);
if (!$swgProperty->validate()) { if (!$swgProperty->validate()) {
return; return;
} }

View File

@ -13,7 +13,6 @@ namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use EXSyst\Component\Swagger\Schema; use EXSyst\Component\Swagger\Schema;
use ReflectionProperty;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@ -39,7 +38,7 @@ class SymfonyConstraintAnnotationReader
/** /**
* Update the given property and schema with defined Symfony constraints. * Update the given property and schema with defined Symfony constraints.
*/ */
public function updateProperty(ReflectionProperty $reflectionProperty, Schema $property) public function updateProperty(\ReflectionProperty $reflectionProperty, Schema $property)
{ {
$annotations = $this->annotationsReader->getPropertyAnnotations($reflectionProperty); $annotations = $this->annotationsReader->getPropertyAnnotations($reflectionProperty);
@ -88,7 +87,7 @@ class SymfonyConstraintAnnotationReader
/** /**
* Set the required properties on the scheme. * Set the required properties on the scheme.
*/ */
private function updateSchemaDefinitionWithRequiredProperty(ReflectionProperty $reflectionProperty) private function updateSchemaDefinitionWithRequiredProperty(\ReflectionProperty $reflectionProperty)
{ {
if (null === $this->schema) { if (null === $this->schema) {
return; return;

View File

@ -33,7 +33,7 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
private $factory; private $factory;
private $namingStrategy; private $namingStrategy;
private $annotationsReader; private $doctrineReader;
public function __construct( public function __construct(
MetadataFactoryInterface $factory, MetadataFactoryInterface $factory,
@ -42,7 +42,7 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
) { ) {
$this->factory = $factory; $this->factory = $factory;
$this->namingStrategy = $namingStrategy; $this->namingStrategy = $namingStrategy;
$this->annotationsReader = new AnnotationsReader($reader); $this->doctrineReader = $reader;
} }
/** /**
@ -59,7 +59,8 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
$groupsExclusion = null !== $model->getGroups() ? new GroupsExclusionStrategy($model->getGroups()) : null; $groupsExclusion = null !== $model->getGroups() ? new GroupsExclusionStrategy($model->getGroups()) : null;
$schema->setType('object'); $schema->setType('object');
$this->annotationsReader->updateDefinition(new \ReflectionClass($className), $schema); $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry);
$annotationsReader->updateDefinition(new \ReflectionClass($className), $schema);
$properties = $schema->getProperties(); $properties = $schema->getProperties();
foreach ($metadata->propertyMetadata as $item) { foreach ($metadata->propertyMetadata as $item) {
@ -69,20 +70,26 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
} }
$name = $this->namingStrategy->translateName($item); $name = $this->namingStrategy->translateName($item);
$groups = $model->getGroups();
if (isset($groups[$name]) && is_array($groups[$name])) {
$groups = $model->getGroups()[$name];
}
// read property options from Swagger Property annotation if it exists // read property options from Swagger Property annotation if it exists
if (null !== $item->reflection) { if (null !== $item->reflection) {
$property = $properties->get($this->annotationsReader->getPropertyName($item->reflection, $name)); $property = $properties->get($annotationsReader->getPropertyName($item->reflection, $name));
$this->annotationsReader->updateProperty($item->reflection, $property); $annotationsReader->updateProperty($item->reflection, $property, $groups);
} else { } else {
$property = $properties->get($name); $property = $properties->get($name);
} }
if (null !== $property->getType()) { if (null !== $property->getType() || null !== $property->getRef()) {
continue; continue;
} }
if (null === $item->type) { if (null === $item->type) {
$properties->remove($name); $properties->remove($name);
continue;
} }
if ($type = $this->getNestedTypeInArray($item)) { if ($type = $this->getNestedTypeInArray($item)) {
@ -108,12 +115,6 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
continue; continue;
} }
if (!isset($model->getGroups()[$name]) || !is_array($model->getGroups()[$name])) {
$groups = $model->getGroups();
} else {
$groups = $model->getGroups()[$name];
}
$property->setRef( $property->setRef(
$this->modelRegistry->register(new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type), $groups)) $this->modelRegistry->register(new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type), $groups))
); );

View File

@ -25,7 +25,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
use ModelRegistryAwareTrait; use ModelRegistryAwareTrait;
private $propertyInfo; private $propertyInfo;
private $annotationsReader; private $doctrineReader;
private $swaggerDefinitionAnnotationReader; private $swaggerDefinitionAnnotationReader;
@ -34,7 +34,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
Reader $reader Reader $reader
) { ) {
$this->propertyInfo = $propertyInfo; $this->propertyInfo = $propertyInfo;
$this->annotationsReader = new AnnotationsReader($reader); $this->doctrineReader = $reader;
} }
public function describe(Model $model, Schema $schema) public function describe(Model $model, Schema $schema)
@ -47,7 +47,9 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
if (null !== $model->getGroups()) { if (null !== $model->getGroups()) {
$context = ['serializer_groups' => $model->getGroups()]; $context = ['serializer_groups' => $model->getGroups()];
} }
$this->annotationsReader->updateDefinition(new \ReflectionClass($class), $schema);
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry);
$annotationsReader->updateDefinition(new \ReflectionClass($class), $schema);
$propertyInfoProperties = $this->propertyInfo->getProperties($class, $context); $propertyInfoProperties = $this->propertyInfo->getProperties($class, $context);
if (null === $propertyInfoProperties) { if (null === $propertyInfoProperties) {
@ -58,14 +60,20 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
// read property options from Swagger Property annotation if it exists // read property options from Swagger Property annotation if it exists
if (property_exists($class, $propertyName)) { if (property_exists($class, $propertyName)) {
$reflectionProperty = new \ReflectionProperty($class, $propertyName); $reflectionProperty = new \ReflectionProperty($class, $propertyName);
$property = $properties->get($this->annotationsReader->getPropertyName($reflectionProperty, $propertyName)); $property = $properties->get($annotationsReader->getPropertyName($reflectionProperty, $propertyName));
$this->annotationsReader->updateProperty($reflectionProperty, $property);
$groups = $model->getGroups();
if (isset($groups[$property]) && is_array($groups[$property])) {
$groups = $model->getGroups()[$property];
}
$annotationsReader->updateProperty($reflectionProperty, $property, $groups);
} else { } else {
$property = $properties->get($propertyName); $property = $properties->get($propertyName);
} }
// If type manually defined // If type manually defined
if (null !== $property->getType()) { if (null !== $property->getType() || null !== $property->getRef()) {
continue; continue;
} }

View File

@ -262,6 +262,7 @@ General PHP objects
If you want to customize the documentation of a property of an object, you can use ``@SWG\Property``:: If you want to customize the documentation of a property of an object, you can use ``@SWG\Property``::
use NelmioApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG; use Swagger\Annotations as SWG;
class User class User
@ -276,6 +277,11 @@ If you want to customize the documentation of a property of an object, you can u
* @SWG\Property(type="string", maxLength=255) * @SWG\Property(type="string", maxLength=255)
*/ */
public $username; public $username;
/**
* @SWG\Property(ref=@Model(type=User::class))
*/
public $friend;
} }
See the `OpenAPI 2.0 specification`__ to see all the available fields of ``@SWG\Property``. See the `OpenAPI 2.0 specification`__ to see all the available fields of ``@SWG\Property``.

View File

@ -15,6 +15,7 @@ use Nelmio\ApiDocBundle\Annotation\Model as ModelAnnotation;
use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry; use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Swagger\Analysis; use Swagger\Analysis;
use Swagger\Annotations\AbstractAnnotation;
use Swagger\Annotations\Items; use Swagger\Annotations\Items;
use Swagger\Annotations\Parameter; use Swagger\Annotations\Parameter;
use Swagger\Annotations\Response; use Swagger\Annotations\Response;
@ -35,7 +36,7 @@ final class ModelRegister
$this->modelRegistry = $modelRegistry; $this->modelRegistry = $modelRegistry;
} }
public function __invoke(Analysis $analysis) public function __invoke(Analysis $analysis, array $parentGroups = null)
{ {
$modelsRegistered = []; $modelsRegistered = [];
foreach ($analysis->annotations as $annotation) { foreach ($analysis->annotations as $annotation) {
@ -43,16 +44,10 @@ final class ModelRegister
if ($annotation instanceof Schema && $annotation->ref instanceof ModelAnnotation) { if ($annotation instanceof Schema && $annotation->ref instanceof ModelAnnotation) {
$model = $annotation->ref; $model = $annotation->ref;
$annotation->ref = $this->modelRegistry->register(new Model($this->createType($model->type), $model->groups)); $annotation->ref = $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups)));
foreach ($annotation->_unmerged as $key => $unmerged) { // It is no longer an unmerged annotation
if ($unmerged === $model) { $this->detach($model, $annotation, $analysis);
unset($annotation->_unmerged[$key]);
break;
}
}
$analysis->annotations->detach($model);
continue; continue;
} }
@ -95,10 +90,25 @@ final class ModelRegister
} }
$annotation->merge([new $annotationClass([ $annotation->merge([new $annotationClass([
'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $model->groups)), 'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups))),
])]); ])]);
// It is no longer an unmerged annotation // It is no longer an unmerged annotation
$this->detach($model, $annotation, $analysis);
}
}
private function getGroups(ModelAnnotation $model, array $parentGroups = null)
{
if (null === $model->groups) {
return $parentGroups;
}
return array_merge($parentGroups ?? [], $model->groups);
}
private function detach(ModelAnnotation $model, AbstractAnnotation $annotation, Analysis $analysis)
{
foreach ($annotation->_unmerged as $key => $unmerged) { foreach ($annotation->_unmerged as $key => $unmerged) {
if ($unmerged === $model) { if ($unmerged === $model) {
unset($annotation->_unmerged[$key]); unset($annotation->_unmerged[$key]);
@ -108,7 +118,6 @@ final class ModelRegister
} }
$analysis->annotations->detach($model); $analysis->annotations->detach($model);
} }
}
private function createType(string $type): Type private function createType(string $type): Type
{ {

View File

@ -12,11 +12,15 @@
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use Nelmio\ApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG; use Swagger\Annotations as SWG;
/** /**
* @Serializer\ExclusionPolicy("all") * @Serializer\ExclusionPolicy("all")
* @SWG\Definition(required={"id", "user"}) * @SWG\Definition(
* required={"id", "user"},
* @SWG\Property(property="virtual", ref=@Model(type=JMSUser::class))
* )
*/ */
class JMSComplex class JMSComplex
{ {
@ -28,7 +32,7 @@ class JMSComplex
private $id; private $id;
/** /**
* @Serializer\Type("Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSUser") * @SWG\Property(ref=@Model(type=JMSUser::class))
* @Serializer\Expose * @Serializer\Expose
* @Serializer\Groups({"details"}) * @Serializer\Groups({"details"})
*/ */

View File

@ -78,6 +78,7 @@ class JMSFunctionalTest extends WebTestCase
'id' => ['type' => 'integer'], 'id' => ['type' => 'integer'],
'user' => ['$ref' => '#/definitions/JMSUser2'], 'user' => ['$ref' => '#/definitions/JMSUser2'],
'name' => ['type' => 'string'], 'name' => ['type' => 'string'],
'virtual' => ['$ref' => '#/definitions/JMSUser'],
], ],
'required' => [ 'required' => [
'id', 'id',