diff --git a/ModelDescriber/Annotations/AnnotationsReader.php b/ModelDescriber/Annotations/AnnotationsReader.php index 0e84cec..5c9f92c 100644 --- a/ModelDescriber/Annotations/AnnotationsReader.php +++ b/ModelDescriber/Annotations/AnnotationsReader.php @@ -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); } } diff --git a/ModelDescriber/Annotations/PropertyPhpDocReader.php b/ModelDescriber/Annotations/PropertyPhpDocReader.php index 1e8c6a3..8cac5cd 100644 --- a/ModelDescriber/Annotations/PropertyPhpDocReader.php +++ b/ModelDescriber/Annotations/PropertyPhpDocReader.php @@ -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; diff --git a/ModelDescriber/Annotations/SwgAnnotationsReader.php b/ModelDescriber/Annotations/SwgAnnotationsReader.php index 3caf41a..9d365df 100644 --- a/ModelDescriber/Annotations/SwgAnnotationsReader.php +++ b/ModelDescriber/Annotations/SwgAnnotationsReader.php @@ -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; } diff --git a/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php b/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php index 6ea8d4f..3b93f8e 100644 --- a/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php +++ b/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php @@ -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; diff --git a/ModelDescriber/JMSModelDescriber.php b/ModelDescriber/JMSModelDescriber.php index 3cc92f3..bf62ac2 100644 --- a/ModelDescriber/JMSModelDescriber.php +++ b/ModelDescriber/JMSModelDescriber.php @@ -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)) ); diff --git a/ModelDescriber/ObjectModelDescriber.php b/ModelDescriber/ObjectModelDescriber.php index e81cde0..33297cb 100644 --- a/ModelDescriber/ObjectModelDescriber.php +++ b/ModelDescriber/ObjectModelDescriber.php @@ -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; } diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index c7f626c..7894af8 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -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``. diff --git a/SwaggerPhp/ModelRegister.php b/SwaggerPhp/ModelRegister.php index 71f62f1..5150a86 100644 --- a/SwaggerPhp/ModelRegister.php +++ b/SwaggerPhp/ModelRegister.php @@ -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)) { diff --git a/Tests/Functional/Entity/JMSComplex.php b/Tests/Functional/Entity/JMSComplex.php index a2d964d..f12185a 100644 --- a/Tests/Functional/Entity/JMSComplex.php +++ b/Tests/Functional/Entity/JMSComplex.php @@ -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"}) */ diff --git a/Tests/Functional/JMSFunctionalTest.php b/Tests/Functional/JMSFunctionalTest.php index 7fff56f..672a31b 100644 --- a/Tests/Functional/JMSFunctionalTest.php +++ b/Tests/Functional/JMSFunctionalTest.php @@ -78,6 +78,7 @@ class JMSFunctionalTest extends WebTestCase 'id' => ['type' => 'integer'], 'user' => ['$ref' => '#/definitions/JMSUser2'], 'name' => ['type' => 'string'], + 'virtual' => ['$ref' => '#/definitions/JMSUser'], ], 'required' => [ 'id',