mirror of
https://github.com/retailcrm/NelmioApiDocBundle.git
synced 2025-02-09 02:59:27 +03:00
Merge pull request #1902 from chrisguitarguy/constraint_groups
Respect Constraint Validation Groups When Describing Models
This commit is contained in:
commit
235963df41
@ -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]);
|
||||
}
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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()) {
|
||||
|
@ -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()) {
|
||||
|
@ -75,6 +75,7 @@
|
||||
<argument type="tagged" tag="nelmio_api_doc.object_model.property_describer" />
|
||||
<argument />
|
||||
<argument type="service" id="serializer.name_converter.metadata_aware" on-invalid="ignore" />
|
||||
<argument>%nelmio_api_doc.use_validation_groups%</argument>
|
||||
|
||||
<tag name="nelmio_api_doc.model_describer" />
|
||||
</service>
|
||||
|
@ -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
|
||||
|
@ -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"})
|
||||
*
|
||||
|
@ -19,7 +19,7 @@ class SymfonyConstraints
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\NotBlank(groups={"test"})
|
||||
*/
|
||||
private $propertyNotBlank;
|
||||
|
||||
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
|
||||
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class SymfonyConstraintsWithValidationGroups
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @Groups("test")
|
||||
* @Assert\NotBlank(groups={"test"})
|
||||
* @Assert\Range(min=1, max=100)
|
||||
*/
|
||||
public $property;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @Assert\Range(min=1, max=100)
|
||||
*/
|
||||
public $propertyInDefaultGroup;
|
||||
}
|
@ -431,8 +431,8 @@ class FunctionalTest extends WebTestCase
|
||||
'properties' => [
|
||||
'propertyNotBlank' => [
|
||||
'type' => 'integer',
|
||||
'maxItems' => '10',
|
||||
'minItems' => '0',
|
||||
'maxItems' => 10,
|
||||
'minItems' => 0,
|
||||
],
|
||||
'propertyNotNull' => [
|
||||
'type' => 'integer',
|
||||
|
@ -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,
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
79
Tests/Functional/ValidationGroupsFunctionalTest.php
Normal file
79
Tests/Functional/ValidationGroupsFunctionalTest.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Tests\Functional;
|
||||
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
|
||||
class ValidationGroupsFunctionalTest extends WebTestCase
|
||||
{
|
||||
protected static function createKernel(array $options = []): KernelInterface
|
||||
{
|
||||
return new TestKernel(TestKernel::USE_VALIDATION_GROUPS);
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
static::createClient([], ['HTTP_HOST' => '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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user