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 5a4c2e3..18879d6 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 cd6a622..1b7f113 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) @@ -175,6 +176,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 dcd38bb..3fbcbfb 100644 --- a/ModelDescriber/Annotations/AnnotationsReader.php +++ b/ModelDescriber/Annotations/AnnotationsReader.php @@ -27,14 +27,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): void diff --git a/ModelDescriber/FormModelDescriber.php b/ModelDescriber/FormModelDescriber.php index 6bff0e4..a0f5c2a 100644 --- a/ModelDescriber/FormModelDescriber.php +++ b/ModelDescriber/FormModelDescriber.php @@ -37,9 +37,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) { @@ -51,6 +56,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) @@ -66,7 +72,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 + ); $annotationsReader->updateDefinition(new \ReflectionClass($class), $schema); $form = $this->formFactory->create($class, null, $model->getOptions() ?? []); diff --git a/ModelDescriber/JMSModelDescriber.php b/ModelDescriber/JMSModelDescriber.php index c81519a..129c432 100644 --- a/ModelDescriber/JMSModelDescriber.php +++ b/ModelDescriber/JMSModelDescriber.php @@ -49,16 +49,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 } $schema->type = 'object'; - $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes); + $annotationsReader = new AnnotationsReader( + $this->doctrineReader, + $this->modelRegistry, + $this->mediaTypes, + $this->useValidationGroups + ); $annotationsReader->updateDefinition(new \ReflectionClass($className), $schema); $isJmsV1 = null !== $this->namingStrategy; diff --git a/ModelDescriber/ObjectModelDescriber.php b/ModelDescriber/ObjectModelDescriber.php index 0aa339a..c4c9b87 100644 --- a/ModelDescriber/ObjectModelDescriber.php +++ b/ModelDescriber/ObjectModelDescriber.php @@ -40,19 +40,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) @@ -68,7 +72,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 + ); $annotationsReader->updateDefinition($reflClass, $schema); $discriminatorMap = $this->doctrineReader->getClassAnnotation($reflClass, DiscriminatorMap::class); diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 9d2c38c..bfc845c 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -75,6 +75,7 @@ + %nelmio_api_doc.use_validation_groups% diff --git a/Tests/Functional/Controller/ApiController.php b/Tests/Functional/Controller/ApiController.php index 4a40897..3de4c5d 100644 --- a/Tests/Functional/Controller/ApiController.php +++ b/Tests/Functional/Controller/ApiController.php @@ -18,6 +18,7 @@ use Nelmio\ApiDocBundle\Annotation\Security; use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article; use Nelmio\ApiDocBundle\Tests\Functional\Entity\CompoundEntity; 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; @@ -179,6 +180,18 @@ class ApiController { } + /** + * @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() + { + } + /** * @OA\Response( * response="200", 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..01057b2 --- /dev/null +++ b/Tests/Functional/Entity/SymfonyConstraintsWithValidationGroups.php @@ -0,0 +1,36 @@ + [ '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 5de564d..fea681b 100644 --- a/Tests/Functional/TestKernel.php +++ b/Tests/Functional/TestKernel.php @@ -19,6 +19,7 @@ use JMS\SerializerBundle\JMSSerializerBundle; use Nelmio\ApiDocBundle\NelmioApiDocBundle; use Nelmio\ApiDocBundle\Tests\Functional\Entity\BazingaUser; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex; +use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture; use Nelmio\ApiDocBundle\Tests\Functional\Entity\PrivateProtectedExposure; use Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber\VirtualTypeClassDoesNotExistsHandlerDefinedDescriber; @@ -32,12 +33,14 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\RouteCollectionBuilder; use Symfony\Component\Serializer\Annotation\SerializedName; +use Symfony\Component\Validator\Constraint; class TestKernel extends Kernel { const USE_JMS = 1; const USE_BAZINGA = 2; const ERROR_ARRAY_ITEMS = 4; + const USE_VALIDATION_GROUPS = 8; use MicroKernelTrait; @@ -173,6 +176,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 [ @@ -263,6 +267,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..611b869 --- /dev/null +++ b/Tests/Functional/ValidationGroupsFunctionalTest.php @@ -0,0 +1,77 @@ + '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) + ); + } +}