Add Validation Group Support to SymfonyConstraintAnnotationReader

This support is gated behind a flag as turning it on seems like it would
be a large backwards incompatible change.
This commit is contained in:
Christopher Davis 2021-11-06 06:16:15 -05:00
parent 0ed76d7d24
commit cc962b72c8
3 changed files with 165 additions and 5 deletions

View File

@ -52,6 +52,6 @@ class AnnotationsReader
{ {
$this->openApiAnnotationsReader->updateProperty($reflection, $property, $serializationGroups); $this->openApiAnnotationsReader->updateProperty($reflection, $property, $serializationGroups);
$this->phpDocReader->updateProperty($reflection, $property); $this->phpDocReader->updateProperty($reflection, $property);
$this->symfonyConstraintAnnotationReader->updateProperty($reflection, $property); $this->symfonyConstraintAnnotationReader->updateProperty($reflection, $property, $serializationGroups);
} }
} }

View File

@ -32,9 +32,15 @@ class SymfonyConstraintAnnotationReader
*/ */
private $schema; private $schema;
public function __construct(Reader $annotationsReader) /**
* @var bool
*/
private $useValidationGroups;
public function __construct(Reader $annotationsReader, bool $useValidationGroups=false)
{ {
$this->annotationsReader = $annotationsReader; $this->annotationsReader = $annotationsReader;
$this->useValidationGroups = $useValidationGroups;
} }
/** /**
@ -42,9 +48,9 @@ class SymfonyConstraintAnnotationReader
* *
* @param \ReflectionProperty|\ReflectionMethod $reflection * @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 $innerAnnotations = $outerAnnotation instanceof Assert\Compound
? $outerAnnotation->constraints ? $outerAnnotation->constraints
: [$outerAnnotation]; : [$outerAnnotation];
@ -175,7 +181,23 @@ class SymfonyConstraintAnnotationReader
/** /**
* @param \ReflectionProperty|\ReflectionMethod $reflection * @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)) { if (\PHP_VERSION_ID >= 80000 && class_exists(Constraint::class)) {
foreach ($reflection->getAttributes(Constraint::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { foreach ($reflection->getAttributes(Constraint::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
@ -189,4 +211,20 @@ class SymfonyConstraintAnnotationReader
yield from $this->annotationsReader->getMethodAnnotations($reflection); 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;
}
} }

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations; namespace Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Component\Validator\Constraint;
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\SymfonyConstraintAnnotationReader; use Nelmio\ApiDocBundle\ModelDescriber\Annotations\SymfonyConstraintAnnotationReader;
use Nelmio\ApiDocBundle\Tests\Helper; use Nelmio\ApiDocBundle\Tests\Helper;
use Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations\Fixture as CustomAssert; 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
);
}
} }