diff --git a/CHANGELOG.md b/CHANGELOG.md index 52e1278..5b3680e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,8 @@ CHANGELOG 3.7.0 ----- -* remove pattern added from the Expression Violation message. * Added `@SerializedName` annotation support and name converters when using Symfony >= 4.2. +* remove pattern added from the Expression Violation message. 3.3.0 ----- diff --git a/ModelDescriber/Annotations/AnnotationsReader.php b/ModelDescriber/Annotations/AnnotationsReader.php index 0b31b1c..62641ab 100644 --- a/ModelDescriber/Annotations/AnnotationsReader.php +++ b/ModelDescriber/Annotations/AnnotationsReader.php @@ -43,15 +43,15 @@ class AnnotationsReader $this->symfonyConstraintAnnotationReader->setSchema($schema); } - public function getPropertyName(\ReflectionProperty $reflectionProperty, string $default): string + public function getPropertyName($reflection, string $default): string { - return $this->openApiAnnotationsReader->getPropertyName($reflectionProperty, $default); + return $this->openApiAnnotationsReader->getPropertyName($reflection, $default); } - public function updateProperty(\ReflectionProperty $reflectionProperty, OA\Property $property, array $serializationGroups = null): void + public function updateProperty($reflection, OA\Property $property, array $serializationGroups = null): void { - $this->openApiAnnotationsReader->updateProperty($reflectionProperty, $property, $serializationGroups); - $this->phpDocReader->updateProperty($reflectionProperty, $property); - $this->symfonyConstraintAnnotationReader->updateProperty($reflectionProperty, $property); + $this->openApiAnnotationsReader->updateProperty($reflection, $property, $serializationGroups); + $this->phpDocReader->updateProperty($reflection, $property); + $this->symfonyConstraintAnnotationReader->updateProperty($reflection, $property); } } diff --git a/ModelDescriber/Annotations/OpenApiAnnotationsReader.php b/ModelDescriber/Annotations/OpenApiAnnotationsReader.php index cac9937..99e31bd 100644 --- a/ModelDescriber/Annotations/OpenApiAnnotationsReader.php +++ b/ModelDescriber/Annotations/OpenApiAnnotationsReader.php @@ -50,29 +50,33 @@ class OpenApiAnnotationsReader $schema->mergeProperties($oaSchema); } - public function getPropertyName(\ReflectionProperty $reflectionProperty, string $default): string + public function getPropertyName($reflection, string $default): string { /** @var OA\Property $oaProperty */ - if (!$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflectionProperty, OA\Property::class)) { + if ($reflection instanceof \ReflectionProperty && !$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflection, OA\Property::class)) { + return $default; + } elseif ($reflection instanceof \ReflectionMethod && !$oaProperty = $this->annotationsReader->getMethodAnnotation($reflection, OA\Property::class)) { return $default; } return OA\UNDEFINED !== $oaProperty->property ? $oaProperty->property : $default; } - public function updateProperty(\ReflectionProperty $reflectionProperty, OA\Property $property, array $serializationGroups = null): void + public function updateProperty($reflection, OA\Property $property, array $serializationGroups = null): void { // In order to have nicer errors - $declaringClass = $reflectionProperty->getDeclaringClass(); + $declaringClass = $reflection->getDeclaringClass(); Analyser::$context = new Context([ 'namespace' => $declaringClass->getNamespaceName(), 'class' => $declaringClass->getShortName(), - 'property' => $reflectionProperty->name, + 'property' => $reflection->name, 'filename' => $declaringClass->getFileName(), ]); /** @var OA\Property $oaProperty */ - if (!$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflectionProperty, OA\Property::class)) { + if ($reflection instanceof \ReflectionProperty && !$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflection, OA\Property::class)) { + return; + } elseif ($reflection instanceof \ReflectionMethod && !$oaProperty = $this->annotationsReader->getMethodAnnotation($reflection, OA\Property::class)) { return; } Analyser::$context = null; diff --git a/ModelDescriber/Annotations/PropertyPhpDocReader.php b/ModelDescriber/Annotations/PropertyPhpDocReader.php index e70d1c2..5234b45 100644 --- a/ModelDescriber/Annotations/PropertyPhpDocReader.php +++ b/ModelDescriber/Annotations/PropertyPhpDocReader.php @@ -32,10 +32,10 @@ class PropertyPhpDocReader /** * Update the Swagger information with information from the DocBlock comment. */ - public function updateProperty(\ReflectionProperty $reflectionProperty, OA\Property $property): void + public function updateProperty($reflection, OA\Property $property): void { try { - $docBlock = $this->docBlockFactory->create($reflectionProperty); + $docBlock = $this->docBlockFactory->create($reflection); } catch (\Exception $e) { // ignore return; diff --git a/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php b/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php index 3580f48..ccd9cb0 100644 --- a/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php +++ b/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php @@ -38,9 +38,13 @@ class SymfonyConstraintAnnotationReader /** * Update the given property and schema with defined Symfony constraints. */ - public function updateProperty(\ReflectionProperty $reflectionProperty, OA\Property $property): void + public function updateProperty($reflection, OA\Property $property): void { - $annotations = $this->annotationsReader->getPropertyAnnotations($reflectionProperty); + if ($reflection instanceof \ReflectionProperty) { + $annotations = $this->annotationsReader->getPropertyAnnotations($reflection); + } else { + $annotations = $this->annotationsReader->getMethodAnnotations($reflection); + } foreach ($annotations as $annotation) { if ($annotation instanceof Assert\NotBlank || $annotation instanceof Assert\NotNull) { @@ -67,7 +71,7 @@ class SymfonyConstraintAnnotationReader $property->minItems = (int) $annotation->min; $property->maxItems = (int) $annotation->max; } elseif ($annotation instanceof Assert\Choice) { - $values = $annotation->callback ? call_user_func(is_array($annotation->callback) ? $annotation->callback : [$reflectionProperty->class, $annotation->callback]) : $annotation->choices; + $values = $annotation->callback ? call_user_func(is_array($annotation->callback) ? $annotation->callback : [$reflection->class, $annotation->callback]) : $annotation->choices; $property->enum = array_values($values); } elseif ($annotation instanceof Assert\Range) { $property->minimum = (int) $annotation->min; diff --git a/ModelDescriber/ObjectModelDescriber.php b/ModelDescriber/ObjectModelDescriber.php index 8036bb8..d42ac93 100644 --- a/ModelDescriber/ObjectModelDescriber.php +++ b/ModelDescriber/ObjectModelDescriber.php @@ -66,8 +66,9 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar $context['serializer_groups'] = array_filter($model->getGroups(), 'is_string'); } + $reflClass = new \ReflectionClass($class); $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes); - $annotationsReader->updateDefinition(new \ReflectionClass($class), $schema); + $annotationsReader->updateDefinition($reflClass, $schema); $propertyInfoProperties = $this->propertyInfo->getProperties($class, $context); if (null === $propertyInfoProperties) { @@ -77,19 +78,22 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar foreach ($propertyInfoProperties as $propertyName) { $serializedName = null !== $this->nameConverter ? $this->nameConverter->normalize($propertyName, $class, null, null !== $model->getGroups() ? ['groups' => $model->getGroups()] : []) : $propertyName; - // read property options from OpenApi Property annotation if it exists - if (property_exists($class, $propertyName)) { - $reflectionProperty = new \ReflectionProperty($class, $propertyName); - $property = Util::getProperty($schema, $annotationsReader->getPropertyName($reflectionProperty, $serializedName)); + $reflections = $this->getReflections($reflClass, $propertyName); - $groups = $model->getGroups(); - if (isset($groups[$propertyName]) && is_array($groups[$propertyName])) { - $groups = $model->getGroups()[$propertyName]; - } + // Check if a custom name is set + foreach ($reflections as $reflection) { + $serializedName = $annotationsReader->getPropertyName($reflection, $serializedName); + } - $annotationsReader->updateProperty($reflectionProperty, $property, $groups); - } else { - $property = Util::getProperty($schema, $serializedName); + $property = Util::getProperty($schema, $annotationsReader->getPropertyName($reflection, $serializedName)); + + // Interpret additional options + $groups = $model->getGroups(); + if (isset($groups[$propertyName]) && is_array($groups[$propertyName])) { + $groups = $model->getGroups()[$propertyName]; + } + foreach ($reflections as $reflection) { + $annotationsReader->updateProperty($reflection, $property, $groups); } // If type manually defined @@ -106,6 +110,34 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar } } + /** + * @return \ReflectionProperty[]|\ReflectionMethod[] + */ + private function getReflections(\ReflectionClass $reflClass, string $propertyName): array + { + $reflections = []; + if ($reflClass->hasProperty($propertyName)) { + $reflections[] = $reflClass->getProperty($propertyName); + } + + $camelProp = $this->camelize($propertyName); + foreach (['', 'get', 'is', 'has', 'can', 'add', 'remove', 'set'] as $prefix) { + if ($reflClass->hasMethod($prefix.$camelProp)) { + $reflections[] = $reflClass->getMethod($prefix.$camelProp); + } + } + + return $reflections; + } + + /** + * Camelizes a given string. + */ + private function camelize(string $string): string + { + return str_replace(' ', '', ucwords(str_replace('_', ' ', $string))); + } + /** * @param Type[] $types */ diff --git a/Tests/Functional/Entity/SymfonyConstraints.php b/Tests/Functional/Entity/SymfonyConstraints.php index 6057d02..1e1bf6e 100644 --- a/Tests/Functional/Entity/SymfonyConstraints.php +++ b/Tests/Functional/Entity/SymfonyConstraints.php @@ -103,6 +103,8 @@ class SymfonyConstraints private $propertyLessThanOrEqual; /** + * @Assert\Count(min="0", max="10") + * * @param int $propertyNotBlank */ public function setPropertyNotBlank(int $propertyNotBlank): void diff --git a/Tests/Functional/Entity/User.php b/Tests/Functional/Entity/User.php index ebd7639..b3d688e 100644 --- a/Tests/Functional/Entity/User.php +++ b/Tests/Functional/Entity/User.php @@ -21,7 +21,7 @@ class User /** * @var int * - * @OA\Property(description = "User id", readOnly = true, title = "userid", example=1, default = null) + * @OA\Property(description = "User id", readOnly = true, title = "userid", default = null) */ private $id; @@ -102,6 +102,7 @@ class User /** * @param int $id + * @OA\Property(example=1) */ public function setId(int $id) { diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php index 5fc871a..dacd12e 100644 --- a/Tests/Functional/FunctionalTest.php +++ b/Tests/Functional/FunctionalTest.php @@ -342,6 +342,8 @@ class FunctionalTest extends WebTestCase 'properties' => [ 'propertyNotBlank' => [ 'type' => 'integer', + 'maxItems' => '10', + 'minItems' => '0', ], 'propertyNotNull' => [ 'type' => 'integer',