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('form.factory'))
|
||||||
->addArgument(new Reference('annotations.reader'))
|
->addArgument(new Reference('annotations.reader'))
|
||||||
->addArgument($container->getParameter('nelmio_api_doc.media_types'))
|
->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]);
|
->addTag('nelmio_api_doc.model_describer', ['priority' => 100]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,10 @@ final class Configuration implements ConfigurationInterface
|
|||||||
|
|
||||||
$rootNode
|
$rootNode
|
||||||
->children()
|
->children()
|
||||||
|
->booleanNode('use_validation_groups')
|
||||||
|
->info('If true, `groups` passed to @Model annotations will be used to limit validation constraints')
|
||||||
|
->defaultFalse()
|
||||||
|
->end()
|
||||||
->arrayNode('documentation')
|
->arrayNode('documentation')
|
||||||
->useAttributeAsKey('key')
|
->useAttributeAsKey('key')
|
||||||
->info('The documentation used as base')
|
->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.areas', array_keys($config['areas']));
|
||||||
$container->setParameter('nelmio_api_doc.media_types', $config['media_types']);
|
$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) {
|
foreach ($config['areas'] as $area => $areaConfig) {
|
||||||
$nameAliases = $this->findNameAliases($config['models']['names'], $area);
|
$nameAliases = $this->findNameAliases($config['models']['names'], $area);
|
||||||
$container->register(sprintf('nelmio_api_doc.generator.%s', $area), ApiDocGenerator::class)
|
$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'),
|
new Reference('annotations.reader'),
|
||||||
$config['media_types'],
|
$config['media_types'],
|
||||||
$jmsNamingStrategy,
|
$jmsNamingStrategy,
|
||||||
|
$container->getParameter('nelmio_api_doc.use_validation_groups'),
|
||||||
])
|
])
|
||||||
->addTag('nelmio_api_doc.model_describer', ['priority' => 50]);
|
->addTag('nelmio_api_doc.model_describer', ['priority' => 50]);
|
||||||
|
|
||||||
|
@ -28,14 +28,21 @@ class AnnotationsReader
|
|||||||
private $openApiAnnotationsReader;
|
private $openApiAnnotationsReader;
|
||||||
private $symfonyConstraintAnnotationReader;
|
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->annotationsReader = $annotationsReader;
|
||||||
$this->modelRegistry = $modelRegistry;
|
$this->modelRegistry = $modelRegistry;
|
||||||
|
|
||||||
$this->phpDocReader = new PropertyPhpDocReader();
|
$this->phpDocReader = new PropertyPhpDocReader();
|
||||||
$this->openApiAnnotationsReader = new OpenApiAnnotationsReader($annotationsReader, $modelRegistry, $mediaTypes);
|
$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
|
public function updateDefinition(\ReflectionClass $reflectionClass, OA\Schema $schema): UpdateClassDefinitionResult
|
||||||
@ -57,7 +64,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,9 +33,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,9 +49,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];
|
||||||
@ -176,7 +182,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) {
|
||||||
@ -190,4 +212,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,9 +40,14 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
|
|||||||
private $formFactory;
|
private $formFactory;
|
||||||
private $doctrineReader;
|
private $doctrineReader;
|
||||||
private $mediaTypes;
|
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->formFactory = $formFactory;
|
||||||
$this->doctrineReader = $reader;
|
$this->doctrineReader = $reader;
|
||||||
if (null === $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);
|
@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->mediaTypes = $mediaTypes;
|
||||||
|
$this->useValidationGroups = $useValidationGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describe(Model $model, OA\Schema $schema)
|
public function describe(Model $model, OA\Schema $schema)
|
||||||
@ -67,7 +73,12 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
|
|||||||
|
|
||||||
$class = $model->getType()->getClassName();
|
$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);
|
$classResult = $annotationsReader->updateDefinition(new \ReflectionClass($class), $schema);
|
||||||
|
|
||||||
if (!$classResult->shouldDescribeModelProperties()) {
|
if (!$classResult->shouldDescribeModelProperties()) {
|
||||||
|
@ -50,16 +50,23 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
|
|||||||
*/
|
*/
|
||||||
private $propertyTypeUseGroupsCache = [];
|
private $propertyTypeUseGroupsCache = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $useValidationGroups;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
MetadataFactoryInterface $factory,
|
MetadataFactoryInterface $factory,
|
||||||
Reader $reader,
|
Reader $reader,
|
||||||
array $mediaTypes,
|
array $mediaTypes,
|
||||||
?PropertyNamingStrategyInterface $namingStrategy = null
|
?PropertyNamingStrategyInterface $namingStrategy = null,
|
||||||
|
bool $useValidationGroups = false
|
||||||
) {
|
) {
|
||||||
$this->factory = $factory;
|
$this->factory = $factory;
|
||||||
$this->namingStrategy = $namingStrategy;
|
$this->namingStrategy = $namingStrategy;
|
||||||
$this->doctrineReader = $reader;
|
$this->doctrineReader = $reader;
|
||||||
$this->mediaTypes = $mediaTypes;
|
$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));
|
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);
|
$classResult = $annotationsReader->updateDefinition(new \ReflectionClass($className), $schema);
|
||||||
|
|
||||||
if (!$classResult->shouldDescribeModelProperties()) {
|
if (!$classResult->shouldDescribeModelProperties()) {
|
||||||
|
@ -41,19 +41,23 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
|
|||||||
private $mediaTypes;
|
private $mediaTypes;
|
||||||
/** @var NameConverterInterface[] */
|
/** @var NameConverterInterface[] */
|
||||||
private $nameConverter;
|
private $nameConverter;
|
||||||
|
/** @var bool */
|
||||||
|
private $useValidationGroups;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
PropertyInfoExtractorInterface $propertyInfo,
|
PropertyInfoExtractorInterface $propertyInfo,
|
||||||
Reader $reader,
|
Reader $reader,
|
||||||
iterable $propertyDescribers,
|
iterable $propertyDescribers,
|
||||||
array $mediaTypes,
|
array $mediaTypes,
|
||||||
NameConverterInterface $nameConverter = null
|
NameConverterInterface $nameConverter = null,
|
||||||
|
bool $useValidationGroups = false
|
||||||
) {
|
) {
|
||||||
$this->propertyInfo = $propertyInfo;
|
$this->propertyInfo = $propertyInfo;
|
||||||
$this->doctrineReader = $reader;
|
$this->doctrineReader = $reader;
|
||||||
$this->propertyDescribers = $propertyDescribers;
|
$this->propertyDescribers = $propertyDescribers;
|
||||||
$this->mediaTypes = $mediaTypes;
|
$this->mediaTypes = $mediaTypes;
|
||||||
$this->nameConverter = $nameConverter;
|
$this->nameConverter = $nameConverter;
|
||||||
|
$this->useValidationGroups = $useValidationGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describe(Model $model, OA\Schema $schema)
|
public function describe(Model $model, OA\Schema $schema)
|
||||||
@ -67,7 +71,12 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
|
|||||||
}
|
}
|
||||||
|
|
||||||
$reflClass = new \ReflectionClass($class);
|
$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);
|
$classResult = $annotationsReader->updateDefinition($reflClass, $schema);
|
||||||
|
|
||||||
if (!$classResult->shouldDescribeModelProperties()) {
|
if (!$classResult->shouldDescribeModelProperties()) {
|
||||||
|
@ -75,6 +75,7 @@
|
|||||||
<argument type="tagged" tag="nelmio_api_doc.object_model.property_describer" />
|
<argument type="tagged" tag="nelmio_api_doc.object_model.property_describer" />
|
||||||
<argument />
|
<argument />
|
||||||
<argument type="service" id="serializer.name_converter.metadata_aware" on-invalid="ignore" />
|
<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" />
|
<tag name="nelmio_api_doc.model_describer" />
|
||||||
</service>
|
</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
|
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.
|
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.
|
As shown in the example above, the bundle provides the ``@Model`` annotation.
|
||||||
@ -290,6 +290,92 @@ This annotation has two options:
|
|||||||
content: new Model(type: User::class, groups: ['non_sensitive_data'])
|
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::
|
.. tip::
|
||||||
|
|
||||||
When used at the root of ``@OA\Response`` and ``@OA\Parameter``, ``@Model`` is automatically nested
|
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\EntityWithObjectType;
|
||||||
use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithRef;
|
use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithRef;
|
||||||
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraints;
|
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\SymfonyDiscriminator;
|
||||||
use Nelmio\ApiDocBundle\Tests\Functional\Entity\User;
|
use Nelmio\ApiDocBundle\Tests\Functional\Entity\User;
|
||||||
use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType;
|
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"})
|
* @Route("/alternate-entity-type", methods={"GET", "POST"})
|
||||||
*
|
*
|
||||||
|
@ -19,7 +19,7 @@ class SymfonyConstraints
|
|||||||
/**
|
/**
|
||||||
* @var int
|
* @var int
|
||||||
*
|
*
|
||||||
* @Assert\NotBlank()
|
* @Assert\NotBlank(groups={"test"})
|
||||||
*/
|
*/
|
||||||
private $propertyNotBlank;
|
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' => [
|
'properties' => [
|
||||||
'propertyNotBlank' => [
|
'propertyNotBlank' => [
|
||||||
'type' => 'integer',
|
'type' => 'integer',
|
||||||
'maxItems' => '10',
|
'maxItems' => 10,
|
||||||
'minItems' => '0',
|
'minItems' => 0,
|
||||||
],
|
],
|
||||||
'propertyNotNull' => [
|
'propertyNotNull' => [
|
||||||
'type' => 'integer',
|
'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\JMSComplex;
|
||||||
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture;
|
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture;
|
||||||
use Nelmio\ApiDocBundle\Tests\Functional\Entity\PrivateProtectedExposure;
|
use Nelmio\ApiDocBundle\Tests\Functional\Entity\PrivateProtectedExposure;
|
||||||
|
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups;
|
||||||
use Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber\VirtualTypeClassDoesNotExistsHandlerDefinedDescriber;
|
use Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber\VirtualTypeClassDoesNotExistsHandlerDefinedDescriber;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle;
|
use Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle;
|
||||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||||
@ -38,6 +39,7 @@ class TestKernel extends Kernel
|
|||||||
const USE_JMS = 1;
|
const USE_JMS = 1;
|
||||||
const USE_BAZINGA = 2;
|
const USE_BAZINGA = 2;
|
||||||
const ERROR_ARRAY_ITEMS = 4;
|
const ERROR_ARRAY_ITEMS = 4;
|
||||||
|
const USE_VALIDATION_GROUPS = 8;
|
||||||
|
|
||||||
use MicroKernelTrait;
|
use MicroKernelTrait;
|
||||||
|
|
||||||
@ -176,6 +178,7 @@ class TestKernel extends Kernel
|
|||||||
|
|
||||||
// Filter routes
|
// Filter routes
|
||||||
$c->loadFromExtension('nelmio_api_doc', [
|
$c->loadFromExtension('nelmio_api_doc', [
|
||||||
|
'use_validation_groups' => boolval($this->flags & self::USE_VALIDATION_GROUPS),
|
||||||
'documentation' => [
|
'documentation' => [
|
||||||
'servers' => [ // from https://github.com/nelmio/NelmioApiDocBundle/issues/1691
|
'servers' => [ // from https://github.com/nelmio/NelmioApiDocBundle/issues/1691
|
||||||
[
|
[
|
||||||
@ -280,6 +283,16 @@ class TestKernel extends Kernel
|
|||||||
'type' => JMSComplex::class,
|
'type' => JMSComplex::class,
|
||||||
'groups' => null,
|
'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\Annotations as OA;
|
||||||
use OpenApi\Generator;
|
use OpenApi\Generator;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
class SymfonyConstraintAnnotationReaderTest extends TestCase
|
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