Added support for constraint attributes

This commit is contained in:
Alexander M. Turek 2021-03-12 00:59:35 +01:00
parent 8f646b8484
commit 16221de418
2 changed files with 133 additions and 48 deletions

View File

@ -14,6 +14,7 @@ namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@ -38,16 +39,12 @@ class SymfonyConstraintAnnotationReader
/** /**
* Update the given property and schema with defined Symfony constraints. * Update the given property and schema with defined Symfony constraints.
*
* @param \ReflectionProperty|\ReflectionMethod $reflection
*/ */
public function updateProperty($reflection, OA\Property $property): void public function updateProperty($reflection, OA\Property $property): void
{ {
if ($reflection instanceof \ReflectionProperty) { foreach ($this->getAnnotations($reflection) as $annotation) {
$annotations = $this->annotationsReader->getPropertyAnnotations($reflection);
} else {
$annotations = $this->annotationsReader->getMethodAnnotations($reflection);
}
foreach ($annotations as $annotation) {
if ($annotation instanceof Assert\NotBlank || $annotation instanceof Assert\NotNull) { if ($annotation instanceof Assert\NotBlank || $annotation instanceof Assert\NotNull) {
// To support symfony/validator < 4.3 // To support symfony/validator < 4.3
if ($annotation instanceof Assert\NotBlank && \property_exists($annotation, 'allowNull') && $annotation->allowNull) { if ($annotation instanceof Assert\NotBlank && \property_exists($annotation, 'allowNull') && $annotation->allowNull) {
@ -133,7 +130,7 @@ class SymfonyConstraintAnnotationReader
} }
/** /**
* @var ReflectionProperty|ReflectionClass * @param \ReflectionProperty|\ReflectionMethod $reflection
*/ */
private function applyEnumFromChoiceConstraint(OA\Schema $property, Assert\Choice $choice, $reflection): void private function applyEnumFromChoiceConstraint(OA\Schema $property, Assert\Choice $choice, $reflection): void
{ {
@ -150,4 +147,22 @@ class SymfonyConstraintAnnotationReader
$setEnumOnThis->enum = array_values($enumValues); $setEnumOnThis->enum = array_values($enumValues);
} }
/**
* @param \ReflectionProperty|\ReflectionMethod $reflection
*/
private function getAnnotations($reflection): \Traversable
{
if (\PHP_VERSION_ID >= 80000) {
foreach ($reflection->getAttributes(Constraint::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
yield $attribute->newInstance();
}
}
if ($reflection instanceof \ReflectionProperty) {
yield from $this->annotationsReader->getPropertyAnnotations($reflection);
} elseif ($reflection instanceof \ReflectionMethod) {
yield from $this->annotationsReader->getMethodAnnotations($reflection);
}
}
} }

View File

@ -47,24 +47,16 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$this->assertEquals($schema->required, ['property1', 'property2']); $this->assertEquals($schema->required, ['property1', 'property2']);
} }
public function testOptionalProperty() /**
* @param object $entity
* @dataProvider provideOptionalProperty
*/
public function testOptionalProperty($entity)
{ {
if (!\property_exists(Assert\NotBlank::class, 'allowNull')) { if (!\property_exists(Assert\NotBlank::class, 'allowNull')) {
$this->markTestSkipped('NotBlank::allowNull was added in symfony/validator 4.3.'); $this->markTestSkipped('NotBlank::allowNull was added in symfony/validator 4.3.');
} }
$entity = new class() {
/**
* @Assert\NotBlank(allowNull = true)
* @Assert\Length(min = 1)
*/
private $property1;
/**
* @Assert\NotBlank()
*/
private $property2;
};
$schema = new OA\Schema([]); $schema = new OA\Schema([]);
$schema->merge([new OA\Property(['property' => 'property1'])]); $schema->merge([new OA\Property(['property' => 'property1'])]);
$schema->merge([new OA\Property(['property' => 'property2'])]); $schema->merge([new OA\Property(['property' => 'property2'])]);
@ -79,21 +71,37 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$this->assertEquals($schema->required, ['property2']); $this->assertEquals($schema->required, ['property2']);
} }
public function testAssertChoiceResultsInNumericArray() public function provideOptionalProperty(): iterable
{ {
define('TEST_ASSERT_CHOICE_STATUSES', [ yield 'Annotations' => [new class() {
1 => 'active',
2 => 'blocked',
]);
$entity = new class() {
/** /**
* @Assert\NotBlank(allowNull = true)
* @Assert\Length(min = 1) * @Assert\Length(min = 1)
* @Assert\Choice(choices=TEST_ASSERT_CHOICE_STATUSES)
*/ */
private $property1; private $property1;
}; /**
* @Assert\NotBlank()
*/
private $property2;
}];
if (\PHP_VERSION_ID >= 80000) {
yield 'Attributes' => [new class() {
#[Assert\NotBlank(allowNull: true)]
#[Assert\Length(min: 1)]
private $property1;
#[Assert\NotBlank]
private $property2;
}];
}
}
/**
* @param object $entity
* @dataProvider provideAssertChoiceResultsInNumericArray
*/
public function testAssertChoiceResultsInNumericArray($entity)
{
$schema = new OA\Schema([]); $schema = new OA\Schema([]);
$schema->merge([new OA\Property(['property' => 'property1'])]); $schema->merge([new OA\Property(['property' => 'property1'])]);
@ -106,15 +114,36 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$this->assertEquals($schema->properties[0]->enum, ['active', 'blocked']); $this->assertEquals($schema->properties[0]->enum, ['active', 'blocked']);
} }
public function testMultipleChoiceConstraintsApplyEnumToItems() public function provideAssertChoiceResultsInNumericArray(): iterable
{ {
$entity = new class() { define('TEST_ASSERT_CHOICE_STATUSES', [
1 => 'active',
2 => 'blocked',
]);
yield 'Annotations' => [new class() {
/** /**
* @Assert\Choice(choices={"one", "two"}, multiple=true) * @Assert\Length(min = 1)
* @Assert\Choice(choices=TEST_ASSERT_CHOICE_STATUSES)
*/ */
private $property1; private $property1;
}; }];
if (\PHP_VERSION_ID >= 80000) {
yield 'Attributes' => [new class() {
#[Assert\Length(min: 1)]
#[Assert\Choice(choices: TEST_ASSERT_CHOICE_STATUSES)]
private $property1;
}];
}
}
/**
* @param object $entity
* @dataProvider provideMultipleChoiceConstraintsApplyEnumToItems
*/
public function testMultipleChoiceConstraintsApplyEnumToItems($entity)
{
$schema = new OA\Schema([]); $schema = new OA\Schema([]);
$schema->merge([new OA\Property(['property' => 'property1'])]); $schema->merge([new OA\Property(['property' => 'property1'])]);
@ -127,18 +156,30 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$this->assertEquals($schema->properties[0]->items->enum, ['one', 'two']); $this->assertEquals($schema->properties[0]->items->enum, ['one', 'two']);
} }
/** public function provideMultipleChoiceConstraintsApplyEnumToItems(): iterable
* @group https://github.com/nelmio/NelmioApiDocBundle/issues/1780
*/
public function testLengthConstraintDoesNotSetMaxLengthIfMaxIsNotSet()
{ {
$entity = new class() { yield 'Annotations' => [new class() {
/** /**
* @Assert\Length(min = 1) * @Assert\Choice(choices={"one", "two"}, multiple=true)
*/ */
private $property1; private $property1;
}; }];
if (\PHP_VERSION_ID >= 80000) {
yield 'Attributes' => [new class() {
#[Assert\Choice(choices: ['one', 'two'], multiple: true)]
private $property1;
}];
}
}
/**
* @param object $entity
* @group https://github.com/nelmio/NelmioApiDocBundle/issues/1780
* @dataProvider provideLengthConstraintDoesNotSetMaxLengthIfMaxIsNotSet
*/
public function testLengthConstraintDoesNotSetMaxLengthIfMaxIsNotSet($entity)
{
$schema = new OA\Schema([]); $schema = new OA\Schema([]);
$schema->merge([new OA\Property(['property' => 'property1'])]); $schema->merge([new OA\Property(['property' => 'property1'])]);
@ -151,18 +192,30 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$this->assertSame(1, $schema->properties[0]->minLength); $this->assertSame(1, $schema->properties[0]->minLength);
} }
/** public function provideLengthConstraintDoesNotSetMaxLengthIfMaxIsNotSet(): iterable
* @group https://github.com/nelmio/NelmioApiDocBundle/issues/1780
*/
public function testLengthConstraintDoesNotSetMinLengthIfMinIsNotSet()
{ {
$entity = new class() { yield 'Annotations' => [new class() {
/** /**
* @Assert\Length(max = 100) * @Assert\Length(min = 1)
*/ */
private $property1; private $property1;
}; }];
if (\PHP_VERSION_ID >= 80000) {
yield 'Attributes' => [new class() {
#[Assert\Length(min: 1)]
private $property1;
}];
}
}
/**
* @param object $entity
* @group https://github.com/nelmio/NelmioApiDocBundle/issues/1780
* @dataProvider provideLengthConstraintDoesNotSetMinLengthIfMinIsNotSet
*/
public function testLengthConstraintDoesNotSetMinLengthIfMinIsNotSet($entity)
{
$schema = new OA\Schema([]); $schema = new OA\Schema([]);
$schema->merge([new OA\Property(['property' => 'property1'])]); $schema->merge([new OA\Property(['property' => 'property1'])]);
@ -174,4 +227,21 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$this->assertSame(OA\UNDEFINED, $schema->properties[0]->minLength); $this->assertSame(OA\UNDEFINED, $schema->properties[0]->minLength);
$this->assertSame(100, $schema->properties[0]->maxLength); $this->assertSame(100, $schema->properties[0]->maxLength);
} }
public function provideLengthConstraintDoesNotSetMinLengthIfMinIsNotSet(): iterable
{
yield 'Annotations' => [new class() {
/**
* @Assert\Length(max = 100)
*/
private $property1;
}];
if (\PHP_VERSION_ID >= 80000) {
yield 'Attributes' => [new class() {
#[Assert\Length(max: 100)]
private $property1;
}];
}
}
} }