diff --git a/ModelDescriber/Annotations/AnnotationsReader.php b/ModelDescriber/Annotations/AnnotationsReader.php index 62641ab..dcd38bb 100644 --- a/ModelDescriber/Annotations/AnnotationsReader.php +++ b/ModelDescriber/Annotations/AnnotationsReader.php @@ -52,6 +52,6 @@ class AnnotationsReader { $this->openApiAnnotationsReader->updateProperty($reflection, $property, $serializationGroups); $this->phpDocReader->updateProperty($reflection, $property); - $this->symfonyConstraintAnnotationReader->updateProperty($reflection, $property); + $this->symfonyConstraintAnnotationReader->updateProperty($reflection, $property, $serializationGroups); } } diff --git a/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php b/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php index 8fa31f6..e6cb574 100644 --- a/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php +++ b/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php @@ -32,9 +32,15 @@ class SymfonyConstraintAnnotationReader */ private $schema; - public function __construct(Reader $annotationsReader) + /** + * @var bool + */ + private $useValidationGroups; + + public function __construct(Reader $annotationsReader, bool $useValidationGroups=false) { $this->annotationsReader = $annotationsReader; + $this->useValidationGroups = $useValidationGroups; } /** @@ -42,9 +48,9 @@ class SymfonyConstraintAnnotationReader * * @param \ReflectionProperty|\ReflectionMethod $reflection */ - public function updateProperty($reflection, OA\Property $property): void + public function updateProperty($reflection, OA\Property $property, ?array $validationGroups = null): void { - foreach ($this->getAnnotations($reflection) as $outerAnnotation) { + foreach ($this->getAnnotations($reflection, $validationGroups) as $outerAnnotation) { $innerAnnotations = $outerAnnotation instanceof Assert\Compound ? $outerAnnotation->constraints : [$outerAnnotation]; @@ -175,7 +181,23 @@ class SymfonyConstraintAnnotationReader /** * @param \ReflectionProperty|\ReflectionMethod $reflection */ - private function getAnnotations($reflection): \Traversable + private function getAnnotations($reflection, ?array $validationGroups): iterable + { + foreach ($this->locateAnnotations($reflection) as $annotation) { + if (! $annotation instanceof Constraint) { + continue; + } + + if (! $this->useValidationGroups || $this->isConstraintInGroup($annotation, $validationGroups)) { + yield $annotation; + } + } + } + + /** + * @param \ReflectionProperty|\ReflectionMethod $reflection + */ + private function locateAnnotations($reflection): \Traversable { if (\PHP_VERSION_ID >= 80000 && class_exists(Constraint::class)) { foreach ($reflection->getAttributes(Constraint::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { @@ -189,4 +211,20 @@ class SymfonyConstraintAnnotationReader yield from $this->annotationsReader->getMethodAnnotations($reflection); } } + + /** + * Check to see if the given constraint is in the provided serialization groups. + * + * If no groups are provided the validator would run in the Constraint::DEFAULT_GROUP, + * and constraints without any `groups` passed to them would be in that same + * default group. So even with a null $validationGroups passed here there still + * has to be a check on the default group. + */ + private function isConstraintInGroup(Constraint $annotation, ?array $validationGroups) : bool + { + return count(array_intersect( + $validationGroups ?: [Constraint::DEFAULT_GROUP], + $annotation->groups + )) > 0; + } } diff --git a/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php b/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php index 9ee7fc8..a864720 100644 --- a/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php +++ b/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php @@ -12,6 +12,7 @@ namespace Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations; use Doctrine\Common\Annotations\AnnotationReader; +use Symfony\Component\Validator\Constraint; use Nelmio\ApiDocBundle\ModelDescriber\Annotations\SymfonyConstraintAnnotationReader; use Nelmio\ApiDocBundle\Tests\Helper; use Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations\Fixture as CustomAssert; @@ -423,4 +424,125 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase }]; } } + + /** + * re-using another provider here, since all constraints land in the default + * group when `group={"someGroup"}` is not set. + * + * @group https://github.com/nelmio/NelmioApiDocBundle/issues/1857 + * @dataProvider provideRangeConstraintDoesNotSetMinimumIfMinIsNotSet + */ + public function testReaderWithValidationGroupsEnabledChecksForDefaultGroupWhenNoSerializationGroupsArePassed($entity) + { + $schema = new OA\Schema([]); + $schema->merge([new OA\Property(['property' => 'property1'])]); + $reader = $this->createConstraintReaderWithValidationGroupsEnabled(); + $reader->setSchema($schema); + + // no serialization groups passed here + $reader->updateProperty( + new \ReflectionProperty($entity, 'property1'), + $schema->properties[0] + ); + + $this->assertSame(10, $schema->properties[0]->maximum, 'should have read constraints in the default group'); + } + + /** + * @group https://github.com/nelmio/NelmioApiDocBundle/issues/1857 + * @dataProvider provideConstraintsWithGroups + */ + public function testReaderWithValidationGroupsEnabledDoesNotReadAnnotationsWithoutDefaultGroupIfNoGroupsArePassed($entity) + { + $schema = new OA\Schema([]); + $schema->merge([ + new OA\Property(['property' => 'property1']), + ]); + $reader = $this->createConstraintReaderWithValidationGroupsEnabled(); + $reader->setSchema($schema); + + // no serialization groups passed here + $reader->updateProperty( + new \ReflectionProperty($entity, 'property1'), + $schema->properties[0] + ); + + $this->assertSame(['property1'], $schema->required, 'should have read constraint in default group'); + $this->assertSame(OA\UNDEFINED, $schema->properties[0]->minimum, 'should not have read constraint in other group'); + } + + /** + * @group https://github.com/nelmio/NelmioApiDocBundle/issues/1857 + * @dataProvider provideConstraintsWithGroups + */ + public function testReaderWithValidationGroupsEnabledReadsOnlyConstraintsWithGroupsProvided($entity) + { + $schema = new OA\Schema([]); + $schema->merge([ + new OA\Property(['property' => 'property1']), + ]); + $reader = $this->createConstraintReaderWithValidationGroupsEnabled(); + $reader->setSchema($schema); + + // no serialization groups passed here + $reader->updateProperty( + new \ReflectionProperty($entity, 'property1'), + $schema->properties[0], + ["other"], + ); + + $this->assertSame(OA\UNDEFINED, $schema->required, 'should not have read constraint in default group'); + $this->assertSame(1, $schema->properties[0]->minimum, 'should have read constraint in other group'); + } + + /** + * @group https://github.com/nelmio/NelmioApiDocBundle/issues/1857 + * @dataProvider provideConstraintsWithGroups + */ + public function testReaderWithValidationGroupsEnabledCanReadFromMultipleValidationGroups($entity) + { + $schema = new OA\Schema([]); + $schema->merge([ + new OA\Property(['property' => 'property1']), + ]); + $reader = $this->createConstraintReaderWithValidationGroupsEnabled(); + $reader->setSchema($schema); + + // no serialization groups passed here + $reader->updateProperty( + new \ReflectionProperty($entity, 'property1'), + $schema->properties[0], + ["other", Constraint::DEFAULT_GROUP], + ); + + $this->assertSame(['property1'], $schema->required, 'should have read constraint in default group'); + $this->assertSame(1, $schema->properties[0]->minimum, 'should have read constraint in other group'); + } + + public function provideConstraintsWithGroups(): iterable + { + yield 'Annotations' => [new class() { + /** + * @Assert\NotBlank() + * @Assert\Range(min=1, groups={"other"}) + */ + private $property1; + }]; + + if (\PHP_VERSION_ID >= 80000) { + yield 'Attributes' => [new class() { + #[Assert\NotBlank()] + #[Assert\Range(min: 1, group: ["other"])] + private $property1; + }]; + } + } + + private function createConstraintReaderWithValidationGroupsEnabled() : SymfonyConstraintAnnotationReader + { + return new SymfonyConstraintAnnotationReader( + new AnnotationReader(), + true + ); + } }