Skip to content

Commit

Permalink
fix: override code based defaults with explicit ones (#2377)
Browse files Browse the repository at this point in the history
## Description

This change allows that defaults that are set in attributes can override
the ones that are read from the code.

This fixes #2330

Closes #2330

## What type of PR is this? (check all applicable)
- [x] Bug Fix
- [ ] Feature
- [ ] Refactor
- [ ] Deprecation
- [ ] Breaking Change
- [ ] Documentation Update
- [ ] CI

## Checklist
- [ ] I have made corresponding changes to the documentation (`docs/`)
- [ ] I have made corresponding changes to the changelog
(`CHANGELOG.md`)

(cherry picked from commit 84c7916)
  • Loading branch information
heiglandreas authored and DjordyKoert committed Nov 7, 2024
1 parent ea1c676 commit 32676fe
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 25 deletions.
4 changes: 4 additions & 0 deletions src/ModelDescriber/Annotations/AnnotationsReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class AnnotationsReader
private PropertyPhpDocReader $phpDocReader;
private OpenApiAnnotationsReader $openApiAnnotationsReader;
private SymfonyConstraintAnnotationReader $symfonyConstraintAnnotationReader;
private ReflectionReader $reflectionReader;

/**
* @param string[] $mediaTypes
Expand All @@ -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);
}
Expand All @@ -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);
}

Expand Down
145 changes: 145 additions & 0 deletions src/ModelDescriber/Annotations/ReflectionReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;

use Nelmio\ApiDocBundle\Util\SetsContextTrait;
use OpenApi\Annotations as OA;
use OpenApi\Generator;

/**
* Read default values of a property from the function or property signature.
*
* This needs to be called before the {@see SymfonyConstraintAnnotationReader},
* otherwise required properties might be considered wrongly.
*
* @internal
*/
final class ReflectionReader
{
use SetsContextTrait;

private ?OA\Schema $schema;

/**
* Update the given property and schema with defined Symfony constraints.
*
* @param \ReflectionProperty|\ReflectionMethod $reflection
*/
public function updateProperty(
$reflection,
OA\Property $property
): void {
// The default has been set by an Annotation or Attribute
// We leave that as it is!
if (Generator::UNDEFINED !== $property->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;
}
}
24 changes: 0 additions & 24 deletions src/ModelDescriber/ObjectModelDescriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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])) {
Expand Down
1 change: 1 addition & 0 deletions tests/Functional/FunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ public function testUserModel(): void
'$ref' => '#/components/schemas/User',
],
'type' => 'array',
'default' => [],
],
'dummy' => [
'$ref' => '#/components/schemas/Dummy2',
Expand Down
2 changes: 1 addition & 1 deletion tests/Functional/JMSFunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down

0 comments on commit 32676fe

Please sign in to comment.