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 EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
/**
* @internal
@ -20,17 +21,19 @@ use EXSyst\Component\Swagger\Schema;
class AnnotationsReader
{
private $annotationsReader;
private $modelRegistry;
private $phpDocReader;
private $swgAnnotationsReader;
private $symfonyConstraintAnnotationReader;
public function __construct(Reader $annotationsReader)
public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry)
{
$this->annotationsReader = $annotationsReader;
$this->modelRegistry = $modelRegistry;
$this->phpDocReader = new PropertyPhpDocReader();
$this->swgAnnotationsReader = new SwgAnnotationsReader($annotationsReader);
$this->swgAnnotationsReader = new SwgAnnotationsReader($annotationsReader, $modelRegistry);
$this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($annotationsReader);
}
@ -45,10 +48,10 @@ class AnnotationsReader
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->swgAnnotationsReader->updateProperty($reflectionProperty, $property);
$this->swgAnnotationsReader->updateProperty($reflectionProperty, $property, $serializationGroups);
$this->symfonyConstraintAnnotationReader->updateProperty($reflectionProperty, $property);
}
}

View File

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

View File

@ -13,8 +13,13 @@ namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\Reader;
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\Property as SwgProperty;
use Swagger\Context;
/**
* @internal
@ -22,10 +27,12 @@ use Swagger\Annotations\Property as SwgProperty;
class SwgAnnotationsReader
{
private $annotationsReader;
private $modelRegister;
public function __construct(Reader $annotationsReader)
public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry)
{
$this->annotationsReader = $annotationsReader;
$this->modelRegister = new ModelRegister($modelRegistry);
}
public function updateDefinition(\ReflectionClass $reflectionClass, Schema $schema)
@ -35,9 +42,14 @@ class SwgAnnotationsReader
return;
}
if (null !== $swgDefinition->required) {
$schema->setRequired($swgDefinition->required);
// Read @Model annotations
$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
@ -50,13 +62,24 @@ class SwgAnnotationsReader
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)) {
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()) {
return;
}

View File

@ -13,7 +13,6 @@ namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\Reader;
use EXSyst\Component\Swagger\Schema;
use ReflectionProperty;
use Symfony\Component\Validator\Constraints as Assert;
/**
@ -39,7 +38,7 @@ class SymfonyConstraintAnnotationReader
/**
* 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);
@ -88,7 +87,7 @@ class SymfonyConstraintAnnotationReader
/**
* Set the required properties on the scheme.
*/
private function updateSchemaDefinitionWithRequiredProperty(ReflectionProperty $reflectionProperty)
private function updateSchemaDefinitionWithRequiredProperty(\ReflectionProperty $reflectionProperty)
{
if (null === $this->schema) {
return;

View File

@ -33,7 +33,7 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
private $factory;
private $namingStrategy;
private $annotationsReader;
private $doctrineReader;
public function __construct(
MetadataFactoryInterface $factory,
@ -42,7 +42,7 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
) {
$this->factory = $factory;
$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;
$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();
foreach ($metadata->propertyMetadata as $item) {
@ -69,20 +70,26 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
}
$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
if (null !== $item->reflection) {
$property = $properties->get($this->annotationsReader->getPropertyName($item->reflection, $name));
$this->annotationsReader->updateProperty($item->reflection, $property);
$property = $properties->get($annotationsReader->getPropertyName($item->reflection, $name));
$annotationsReader->updateProperty($item->reflection, $property, $groups);
} else {
$property = $properties->get($name);
}
if (null !== $property->getType()) {
if (null !== $property->getType() || null !== $property->getRef()) {
continue;
}
if (null === $item->type) {
$properties->remove($name);
continue;
}
if ($type = $this->getNestedTypeInArray($item)) {
@ -108,12 +115,6 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
continue;
}
if (!isset($model->getGroups()[$name]) || !is_array($model->getGroups()[$name])) {
$groups = $model->getGroups();
} else {
$groups = $model->getGroups()[$name];
}
$property->setRef(
$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;
private $propertyInfo;
private $annotationsReader;
private $doctrineReader;
private $swaggerDefinitionAnnotationReader;
@ -34,7 +34,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
Reader $reader
) {
$this->propertyInfo = $propertyInfo;
$this->annotationsReader = new AnnotationsReader($reader);
$this->doctrineReader = $reader;
}
public function describe(Model $model, Schema $schema)
@ -47,7 +47,9 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
if (null !== $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);
if (null === $propertyInfoProperties) {
@ -58,14 +60,20 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
// read property options from Swagger Property annotation if it exists
if (property_exists($class, $propertyName)) {
$reflectionProperty = new \ReflectionProperty($class, $propertyName);
$property = $properties->get($this->annotationsReader->getPropertyName($reflectionProperty, $propertyName));
$this->annotationsReader->updateProperty($reflectionProperty, $property);
$property = $properties->get($annotationsReader->getPropertyName($reflectionProperty, $propertyName));
$groups = $model->getGroups();
if (isset($groups[$property]) && is_array($groups[$property])) {
$groups = $model->getGroups()[$property];
}
$annotationsReader->updateProperty($reflectionProperty, $property, $groups);
} else {
$property = $properties->get($propertyName);
}
// If type manually defined
if (null !== $property->getType()) {
if (null !== $property->getType() || null !== $property->getRef()) {
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``::
use NelmioApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG;
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)
*/
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``.

View File

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

View File

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

View File

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