diff --git a/DependencyInjection/Compiler/ConfigurationPass.php b/DependencyInjection/Compiler/ConfigurationPass.php
index 2bfa5ec..674062f 100644
--- a/DependencyInjection/Compiler/ConfigurationPass.php
+++ b/DependencyInjection/Compiler/ConfigurationPass.php
@@ -31,6 +31,7 @@ final class ConfigurationPass implements CompilerPassInterface
->addArgument(new Reference('form.factory'))
->addArgument(new Reference('annotations.reader'))
->addArgument($container->getParameter('nelmio_api_doc.media_types'))
+ ->addArgument($container->getParameter('nelmio_api_doc.use_validation_groups'))
->addTag('nelmio_api_doc.model_describer', ['priority' => 100]);
}
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index e23380d..7c469cd 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -29,6 +29,10 @@ final class Configuration implements ConfigurationInterface
$rootNode
->children()
+ ->booleanNode('use_validation_groups')
+ ->info('If true, `groups` passed to @Model annotations will be used to limit validation constraints')
+ ->defaultFalse()
+ ->end()
->arrayNode('documentation')
->useAttributeAsKey('key')
->info('The documentation used as base')
diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php
index 2040acb..caf2cc2 100644
--- a/DependencyInjection/NelmioApiDocExtension.php
+++ b/DependencyInjection/NelmioApiDocExtension.php
@@ -64,6 +64,7 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
$container->setParameter('nelmio_api_doc.areas', array_keys($config['areas']));
$container->setParameter('nelmio_api_doc.media_types', $config['media_types']);
+ $container->setParameter('nelmio_api_doc.use_validation_groups', $config['use_validation_groups']);
foreach ($config['areas'] as $area => $areaConfig) {
$nameAliases = $this->findNameAliases($config['models']['names'], $area);
$container->register(sprintf('nelmio_api_doc.generator.%s', $area), ApiDocGenerator::class)
@@ -176,6 +177,7 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
new Reference('annotations.reader'),
$config['media_types'],
$jmsNamingStrategy,
+ $container->getParameter('nelmio_api_doc.use_validation_groups'),
])
->addTag('nelmio_api_doc.model_describer', ['priority' => 50]);
diff --git a/ModelDescriber/Annotations/AnnotationsReader.php b/ModelDescriber/Annotations/AnnotationsReader.php
index 261bd84..85f496d 100644
--- a/ModelDescriber/Annotations/AnnotationsReader.php
+++ b/ModelDescriber/Annotations/AnnotationsReader.php
@@ -28,14 +28,21 @@ class AnnotationsReader
private $openApiAnnotationsReader;
private $symfonyConstraintAnnotationReader;
- public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry, array $mediaTypes)
- {
+ public function __construct(
+ Reader $annotationsReader,
+ ModelRegistry $modelRegistry,
+ array $mediaTypes,
+ bool $useValidationGroups = false
+ ) {
$this->annotationsReader = $annotationsReader;
$this->modelRegistry = $modelRegistry;
$this->phpDocReader = new PropertyPhpDocReader();
$this->openApiAnnotationsReader = new OpenApiAnnotationsReader($annotationsReader, $modelRegistry, $mediaTypes);
- $this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($annotationsReader);
+ $this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(
+ $annotationsReader,
+ $useValidationGroups
+ );
}
public function updateDefinition(\ReflectionClass $reflectionClass, OA\Schema $schema): UpdateClassDefinitionResult
@@ -57,7 +64,7 @@ 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 6b9e771..861bec3 100644
--- a/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php
+++ b/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php
@@ -33,9 +33,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;
}
/**
@@ -43,9 +49,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];
@@ -176,7 +182,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) {
@@ -190,4 +212,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/ModelDescriber/FormModelDescriber.php b/ModelDescriber/FormModelDescriber.php
index 0928aec..82bef1a 100644
--- a/ModelDescriber/FormModelDescriber.php
+++ b/ModelDescriber/FormModelDescriber.php
@@ -40,9 +40,14 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
private $formFactory;
private $doctrineReader;
private $mediaTypes;
+ private $useValidationGroups;
- public function __construct(FormFactoryInterface $formFactory = null, Reader $reader = null, array $mediaTypes = null)
- {
+ public function __construct(
+ FormFactoryInterface $formFactory = null,
+ Reader $reader = null,
+ array $mediaTypes = null,
+ bool $useValidationGroups = false
+ ) {
$this->formFactory = $formFactory;
$this->doctrineReader = $reader;
if (null === $reader) {
@@ -54,6 +59,7 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
@trigger_error(sprintf('Not passing media types to the constructor of %s is deprecated since version 4.1 and won\'t be allowed in version 5.', self::class), E_USER_DEPRECATED);
}
$this->mediaTypes = $mediaTypes;
+ $this->useValidationGroups = $useValidationGroups;
}
public function describe(Model $model, OA\Schema $schema)
@@ -67,7 +73,12 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
$class = $model->getType()->getClassName();
- $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes);
+ $annotationsReader = new AnnotationsReader(
+ $this->doctrineReader,
+ $this->modelRegistry,
+ $this->mediaTypes,
+ $this->useValidationGroups
+ );
$classResult = $annotationsReader->updateDefinition(new \ReflectionClass($class), $schema);
if (!$classResult->shouldDescribeModelProperties()) {
diff --git a/ModelDescriber/JMSModelDescriber.php b/ModelDescriber/JMSModelDescriber.php
index e589d89..264c813 100644
--- a/ModelDescriber/JMSModelDescriber.php
+++ b/ModelDescriber/JMSModelDescriber.php
@@ -50,16 +50,23 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
*/
private $propertyTypeUseGroupsCache = [];
+ /**
+ * @var bool
+ */
+ private $useValidationGroups;
+
public function __construct(
MetadataFactoryInterface $factory,
Reader $reader,
array $mediaTypes,
- ?PropertyNamingStrategyInterface $namingStrategy = null
+ ?PropertyNamingStrategyInterface $namingStrategy = null,
+ bool $useValidationGroups = false
) {
$this->factory = $factory;
$this->namingStrategy = $namingStrategy;
$this->doctrineReader = $reader;
$this->mediaTypes = $mediaTypes;
+ $this->useValidationGroups = $useValidationGroups;
}
/**
@@ -73,7 +80,12 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
throw new \InvalidArgumentException(sprintf('No metadata found for class %s.', $className));
}
- $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes);
+ $annotationsReader = new AnnotationsReader(
+ $this->doctrineReader,
+ $this->modelRegistry,
+ $this->mediaTypes,
+ $this->useValidationGroups
+ );
$classResult = $annotationsReader->updateDefinition(new \ReflectionClass($className), $schema);
if (!$classResult->shouldDescribeModelProperties()) {
diff --git a/ModelDescriber/ObjectModelDescriber.php b/ModelDescriber/ObjectModelDescriber.php
index e10dba6..925ea5c 100644
--- a/ModelDescriber/ObjectModelDescriber.php
+++ b/ModelDescriber/ObjectModelDescriber.php
@@ -41,19 +41,23 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
private $mediaTypes;
/** @var NameConverterInterface[] */
private $nameConverter;
+ /** @var bool */
+ private $useValidationGroups;
public function __construct(
PropertyInfoExtractorInterface $propertyInfo,
Reader $reader,
iterable $propertyDescribers,
array $mediaTypes,
- NameConverterInterface $nameConverter = null
+ NameConverterInterface $nameConverter = null,
+ bool $useValidationGroups = false
) {
$this->propertyInfo = $propertyInfo;
$this->doctrineReader = $reader;
$this->propertyDescribers = $propertyDescribers;
$this->mediaTypes = $mediaTypes;
$this->nameConverter = $nameConverter;
+ $this->useValidationGroups = $useValidationGroups;
}
public function describe(Model $model, OA\Schema $schema)
@@ -67,7 +71,12 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
}
$reflClass = new \ReflectionClass($class);
- $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes);
+ $annotationsReader = new AnnotationsReader(
+ $this->doctrineReader,
+ $this->modelRegistry,
+ $this->mediaTypes,
+ $this->useValidationGroups
+ );
$classResult = $annotationsReader->updateDefinition($reflClass, $schema);
if (!$classResult->shouldDescribeModelProperties()) {
diff --git a/Resources/config/services.xml b/Resources/config/services.xml
index 99be6ea..b107302 100644
--- a/Resources/config/services.xml
+++ b/Resources/config/services.xml
@@ -75,6 +75,7 @@
+ %nelmio_api_doc.use_validation_groups%
diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst
index de41cae..ba2838d 100644
--- a/Resources/doc/index.rst
+++ b/Resources/doc/index.rst
@@ -235,7 +235,7 @@ The normal PHPDoc block on the controller method is used for the summary and des
However, unlike in those examples, when using this bundle you don't need to specify paths and you can easily document models as well as some
other properties described below as they can be automatically be documented using the Symfony integration.
-Use models
+Use Models
----------
As shown in the example above, the bundle provides the ``@Model`` annotation.
@@ -261,7 +261,7 @@ This annotation has two options:
*/
.. code-block:: php-attributes
-
+
#[OA\Response(
response: 200,
description: 'Successful response',
@@ -290,6 +290,92 @@ This annotation has two options:
content: new Model(type: User::class, groups: ['non_sensitive_data'])
)]
+* ``groups`` may also be used to specify the constraint validation groups used
+ (de)serialize your model, but this must be enabled in configuration::
+
+.. code-block:: yaml
+
+ nelmio_api_doc:
+ use_validation_groups: true
+ # ...
+
+With this enabled, groups set in the model will apply to both serializer
+properties and validator constraints. Take the model class below:
+
+.. configuration-block::
+
+ .. code-block:: php-annotations
+
+ use Symfony\Component\Serializer\Annotation\Group;
+ use Symfony\Component\Validator\Constraints as Assert;
+
+ class UserDto
+ {
+ /**
+ * @Group({"default", "create", "update"})
+ * @Assert\NotBlank(groups={"default", "create"})
+ */
+ public string $username;
+ }
+
+ .. code-block:: php-attributes
+
+ use Symfony\Component\Serializer\Annotation\Group;
+ use Symfony\Component\Validator\Constraints as Assert;
+
+ class UserDto
+ {
+ #[Group(["default", "create", "update"])
+ #[Assert\NotBlank(groups: ["default", "create"])
+ public string $username;
+ }
+
+The ``NotBlank`` constraint will apply only to the ``default`` and ``create``
+group, but not ``update``. In more practical terms: the `username` property
+would show as ``required`` for both model create and default, but not update.
+When using code generators to build API clients, this often translates into
+client side validation and types. ``NotBlank`` adding ``required`` will cause
+that property type to not be nullable, for example.
+
+.. configuration-block::
+
+ .. code-block:: php-annotations
+
+ use OpenApi\Annotations as OA;
+
+ /**
+ * shows `username` as `required` in the OpenAPI schema (not nullable)
+ * @OA\Response(
+ * response=200,
+ * @Model(type=UserDto::class, groups={"default"})
+ * )
+ */
+
+ /**
+ * Similarly, this will make the username `required` in the create
+ * schema
+ * @OA\RequestBody(@Model(type=UserDto::class, groups={"create"}))
+ */
+
+ /**
+ * But for updates, the `username` property will not be required
+ * @OA\RequestBody(@Model(type=UserDto::class, groups={"update"}))
+ */
+
+ .. code-block:: php-attributes
+
+ use OpenApi\Attributes as OA;
+
+ // shows `username` as `required` in the OpenAPI schema (not nullable)
+ #[OA\Response(response: 200, content: new Model(type: UserDto::class, groups: ["default"]))]
+
+ // Similarly, this will make the username `required` in the create schema
+ #[OA\RequestBody(new Model(type: UserDto::class, groups: ["create"]))]
+
+ // But for updates, the `username` property will not be required
+ #[OA\RequestBody(new Model(type: UserDto::class, groups: ["update"]))]
+
+
.. tip::
When used at the root of ``@OA\Response`` and ``@OA\Parameter``, ``@Model`` is automatically nested
diff --git a/Tests/Functional/Controller/ApiController80.php b/Tests/Functional/Controller/ApiController80.php
index bb7031c..9520a4d 100644
--- a/Tests/Functional/Controller/ApiController80.php
+++ b/Tests/Functional/Controller/ApiController80.php
@@ -21,6 +21,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithAlternateType;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithObjectType;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithRef;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraints;
+use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyDiscriminator;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\User;
use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType;
@@ -278,6 +279,18 @@ class ApiController80
{
}
+ /**
+ * @Route("/swagger/symfonyConstraintsWithValidationGroups", methods={"GET"})
+ * @OA\Response(
+ * response="201",
+ * description="Used for symfony constraints with validation groups test",
+ * @Model(type=SymfonyConstraintsWithValidationGroups::class, groups={"test"})
+ * )
+ */
+ public function symfonyConstraintsWithGroupsAction()
+ {
+ }
+
/**
* @Route("/alternate-entity-type", methods={"GET", "POST"})
*
diff --git a/Tests/Functional/Entity/SymfonyConstraints.php b/Tests/Functional/Entity/SymfonyConstraints.php
index 05e65c9..86e3d16 100644
--- a/Tests/Functional/Entity/SymfonyConstraints.php
+++ b/Tests/Functional/Entity/SymfonyConstraints.php
@@ -19,7 +19,7 @@ class SymfonyConstraints
/**
* @var int
*
- * @Assert\NotBlank()
+ * @Assert\NotBlank(groups={"test"})
*/
private $propertyNotBlank;
diff --git a/Tests/Functional/Entity/SymfonyConstraintsWithValidationGroups.php b/Tests/Functional/Entity/SymfonyConstraintsWithValidationGroups.php
new file mode 100644
index 0000000..65c2896
--- /dev/null
+++ b/Tests/Functional/Entity/SymfonyConstraintsWithValidationGroups.php
@@ -0,0 +1,34 @@
+ [
'propertyNotBlank' => [
'type' => 'integer',
- 'maxItems' => '10',
- 'minItems' => '0',
+ 'maxItems' => 10,
+ 'minItems' => 0,
],
'propertyNotNull' => [
'type' => 'integer',
diff --git a/Tests/Functional/TestKernel.php b/Tests/Functional/TestKernel.php
index c9b8ee2..321d7c8 100644
--- a/Tests/Functional/TestKernel.php
+++ b/Tests/Functional/TestKernel.php
@@ -21,6 +21,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\BazingaUser;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\PrivateProtectedExposure;
+use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups;
use Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber\VirtualTypeClassDoesNotExistsHandlerDefinedDescriber;
use Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
@@ -38,6 +39,7 @@ class TestKernel extends Kernel
const USE_JMS = 1;
const USE_BAZINGA = 2;
const ERROR_ARRAY_ITEMS = 4;
+ const USE_VALIDATION_GROUPS = 8;
use MicroKernelTrait;
@@ -176,6 +178,7 @@ class TestKernel extends Kernel
// Filter routes
$c->loadFromExtension('nelmio_api_doc', [
+ 'use_validation_groups' => boolval($this->flags & self::USE_VALIDATION_GROUPS),
'documentation' => [
'servers' => [ // from https://github.com/nelmio/NelmioApiDocBundle/issues/1691
[
@@ -280,6 +283,16 @@ class TestKernel extends Kernel
'type' => JMSComplex::class,
'groups' => null,
],
+ [
+ 'alias' => 'SymfonyConstraintsTestGroup',
+ 'type' => SymfonyConstraintsWithValidationGroups::class,
+ 'groups' => ['test'],
+ ],
+ [
+ 'alias' => 'SymfonyConstraintsDefaultGroup',
+ 'type' => SymfonyConstraintsWithValidationGroups::class,
+ 'groups' => null,
+ ],
],
],
]);
diff --git a/Tests/Functional/ValidationGroupsFunctionalTest.php b/Tests/Functional/ValidationGroupsFunctionalTest.php
new file mode 100644
index 0000000..70d018e
--- /dev/null
+++ b/Tests/Functional/ValidationGroupsFunctionalTest.php
@@ -0,0 +1,79 @@
+ 'api.example.com']);
+ }
+
+ public function testConstraintGroupsAreRespectedWhenDescribingModels()
+ {
+ $expected = [
+ 'required' => [
+ 'property',
+ ],
+ 'properties' => [
+ 'property' => [
+ 'type' => 'integer',
+ // the min/max constraint is in the default group only and shouldn't
+ // be read here with validation groups turned on
+ ],
+ ],
+ 'type' => 'object',
+ 'schema' => 'SymfonyConstraintsTestGroup',
+ ];
+
+ $this->assertEquals(
+ $expected,
+ json_decode($this->getModel('SymfonyConstraintsTestGroup')->toJson(), true)
+ );
+ }
+
+ public function testConstraintDefaultGroupsAreRespectedWhenReadingAnnotations()
+ {
+ $expected = [
+ 'properties' => [
+ 'property' => [
+ 'type' => 'integer',
+ // min/max will be read here as they are in th e default group
+ 'maximum' => 100,
+ 'minimum' => 1,
+ ],
+ 'propertyInDefaultGroup' => [
+ 'type' => 'integer',
+ // min/max will be read here as they are in th e default group
+ 'maximum' => 100,
+ 'minimum' => 1,
+ ],
+ ],
+ 'type' => 'object',
+ 'schema' => 'SymfonyConstraintsDefaultGroup',
+ ];
+
+ $this->assertEquals(
+ $expected,
+ json_decode($this->getModel('SymfonyConstraintsDefaultGroup')->toJson(), true)
+ );
+ }
+}
diff --git a/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php b/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php
index 6037df9..b95876e 100644
--- a/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php
+++ b/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php
@@ -18,6 +18,7 @@ use Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations\Fixture as CustomAssert
use OpenApi\Annotations as OA;
use OpenApi\Generator;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints as Assert;
class SymfonyConstraintAnnotationReaderTest extends TestCase
@@ -424,4 +425,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(Generator::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(Generator::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, groups: ['other'])]
+ private $property1;
+ }];
+ }
+ }
+
+ private function createConstraintReaderWithValidationGroupsEnabled(): SymfonyConstraintAnnotationReader
+ {
+ return new SymfonyConstraintAnnotationReader(
+ new AnnotationReader(),
+ true
+ );
+ }
}