diff --git a/src/ModelDescriber/Annotations/AnnotationsReader.php b/src/ModelDescriber/Annotations/AnnotationsReader.php index f5d0f80e1..4128c7708 100644 --- a/src/ModelDescriber/Annotations/AnnotationsReader.php +++ b/src/ModelDescriber/Annotations/AnnotationsReader.php @@ -23,6 +23,7 @@ class AnnotationsReader private PropertyPhpDocReader $phpDocReader; private OpenApiAnnotationsReader $openApiAnnotationsReader; private SymfonyConstraintAnnotationReader $symfonyConstraintAnnotationReader; + private ReflectionReader $reflectionReader; /** * @param string[] $mediaTypes @@ -35,12 +36,14 @@ public function __construct( $this->phpDocReader = new PropertyPhpDocReader(); $this->openApiAnnotationsReader = new OpenApiAnnotationsReader($modelRegistry, $mediaTypes); $this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($useValidationGroups); + $this->reflectionReader = new ReflectionReader(); } public function updateDefinition(\ReflectionClass $reflectionClass, OA\Schema $schema): bool { $this->openApiAnnotationsReader->updateSchema($reflectionClass, $schema); $this->symfonyConstraintAnnotationReader->setSchema($schema); + $this->reflectionReader->setSchema($schema); return $this->shouldDescribeModelProperties($schema); } @@ -61,6 +64,7 @@ public function updateProperty($reflection, OA\Property $property, ?array $seria { $this->openApiAnnotationsReader->updateProperty($reflection, $property, $serializationGroups); $this->phpDocReader->updateProperty($reflection, $property); + $this->reflectionReader->updateProperty($reflection, $property); $this->symfonyConstraintAnnotationReader->updateProperty($reflection, $property, $serializationGroups); } diff --git a/src/ModelDescriber/Annotations/ReflectionReader.php b/src/ModelDescriber/Annotations/ReflectionReader.php new file mode 100644 index 000000000..4ea3f26f9 --- /dev/null +++ b/src/ModelDescriber/Annotations/ReflectionReader.php @@ -0,0 +1,145 @@ +default) { + return; + } + + $serializedName = $reflection->getName(); + foreach (['get', 'is', 'has', 'can', 'add', 'remove', 'set'] as $prefix) { + if (0 === strpos($serializedName, $prefix)) { + $serializedName = substr($serializedName, strlen($prefix)); + } + } + + if ($reflection instanceof \ReflectionMethod) { + $methodDefault = $this->getDefaultFromMethodReflection($reflection); + if (Generator::UNDEFINED !== $methodDefault) { + $property->default = $methodDefault; + + return; + } + } + + if ($reflection instanceof \ReflectionProperty) { + $methodDefault = $this->getDefaultFromPropertyReflection($reflection); + if (Generator::UNDEFINED !== $methodDefault) { + $property->default = $methodDefault; + + return; + } + } + // Fix for https://github.com/nelmio/NelmioApiDocBundle/issues/2222 + // Promoted properties with a value initialized by the constructor are not considered to have a default value + // and are therefore not returned by ReflectionClass::getDefaultProperties(); see https://bugs.php.net/bug.php?id=81386 + $reflClassConstructor = $reflection->getDeclaringClass()->getConstructor(); + $reflClassConstructorParameters = null !== $reflClassConstructor ? $reflClassConstructor->getParameters() : []; + foreach ($reflClassConstructorParameters as $parameter) { + if ($parameter->name !== $serializedName) { + continue; + } + if (!$parameter->isDefaultValueAvailable()) { + continue; + } + + if (null === $this->schema) { + continue; + } + + if (!Generator::isDefault($property->default)) { + continue; + } + + $property->default = $parameter->getDefaultValue(); + } + } + + public function setSchema(OA\Schema $schema): void + { + $this->schema = $schema; + } + + /** + * @return mixed|string + */ + private function getDefaultFromMethodReflection(\ReflectionMethod $reflection) + { + if (0 !== strpos($reflection->name, 'set')) { + return Generator::UNDEFINED; + } + + if (1 !== $reflection->getNumberOfParameters()) { + return Generator::UNDEFINED; + } + + $param = $reflection->getParameters()[0]; + + if (!$param->isDefaultValueAvailable()) { + return Generator::UNDEFINED; + } + + if (null === $param->getDefaultValue()) { + return Generator::UNDEFINED; + } + + return $param->getDefaultValue(); + } + + /** + * @return mixed|string + */ + public function getDefaultFromPropertyReflection(\ReflectionProperty $reflection) + { + $propertyName = $reflection->name; + if (!$reflection->getDeclaringClass()->hasProperty($propertyName)) { + return Generator::UNDEFINED; + } + + $defaultValue = $reflection->getDeclaringClass()->getDefaultProperties()[$propertyName] ?? null; + + if (null === $defaultValue) { + return Generator::UNDEFINED; + } + + return $defaultValue; + } +} diff --git a/src/ModelDescriber/ObjectModelDescriber.php b/src/ModelDescriber/ObjectModelDescriber.php index b7b124074..47253a84a 100644 --- a/src/ModelDescriber/ObjectModelDescriber.php +++ b/src/ModelDescriber/ObjectModelDescriber.php @@ -120,23 +120,6 @@ public function describe(Model $model, OA\Schema $schema) // The SerializerExtractor does expose private/protected properties for some reason, so we eliminate them here $propertyInfoProperties = array_intersect($propertyInfoProperties, $this->propertyInfo->getProperties($class, []) ?? []); - $defaultValues = array_filter($reflClass->getDefaultProperties(), static function ($value) { - return null !== $value; - }); - - // Fix for https://github.com/nelmio/NelmioApiDocBundle/issues/2222 - // Promoted properties with a value initialized by the constructor are not considered to have a default value - // and are therefore not returned by ReflectionClass::getDefaultProperties(); see https://bugs.php.net/bug.php?id=81386 - $reflClassConstructor = $reflClass->getConstructor(); - $reflClassConstructorParameters = null !== $reflClassConstructor ? $reflClassConstructor->getParameters() : []; - foreach ($reflClassConstructorParameters as $parameter) { - if (!$parameter->isDefaultValueAvailable()) { - continue; - } - - $defaultValues[$parameter->name] = $parameter->getDefaultValue(); - } - foreach ($propertyInfoProperties as $propertyName) { $serializedName = null !== $this->nameConverter ? $this->nameConverter->normalize($propertyName, $class, null, $model->getSerializationContext()) : $propertyName; @@ -149,13 +132,6 @@ public function describe(Model $model, OA\Schema $schema) $property = Util::getProperty($schema, $serializedName); - // Fix for https://github.com/nelmio/NelmioApiDocBundle/issues/2222 - // Property default value has to be set before SymfonyConstraintAnnotationReader::processPropertyAnnotations() - // is called to prevent wrongly detected required properties - if (Generator::UNDEFINED === $property->default && array_key_exists($propertyName, $defaultValues)) { - $property->default = $defaultValues[$propertyName]; - } - // Interpret additional options $groups = $model->getGroups(); if (isset($groups[$propertyName]) && is_array($groups[$propertyName])) { diff --git a/tests/Functional/FunctionalTest.php b/tests/Functional/FunctionalTest.php index 02bbfccee..2f1481c37 100644 --- a/tests/Functional/FunctionalTest.php +++ b/tests/Functional/FunctionalTest.php @@ -226,6 +226,7 @@ public function testUserModel(): void '$ref' => '#/components/schemas/User', ], 'type' => 'array', + 'default' => [], ], 'dummy' => [ '$ref' => '#/components/schemas/Dummy2', diff --git a/tests/Functional/JMSFunctionalTest.php b/tests/Functional/JMSFunctionalTest.php index 6d3d4f042..39981f662 100644 --- a/tests/Functional/JMSFunctionalTest.php +++ b/tests/Functional/JMSFunctionalTest.php @@ -328,9 +328,9 @@ public function testNamingStrategyWithConstraints(): void 'type' => 'string', 'maxLength' => 10, 'minLength' => 3, + 'default' => 'default' ], ], - 'required' => ['beautifulName'], 'schema' => 'JMSNamingStrategyConstraints', ], json_decode($this->getModel('JMSNamingStrategyConstraints')->toJson(), true)); }