2018-01-25 14:59:48 +01:00
|
|
|
<?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\ModelDescriber\Annotations;
|
|
|
|
|
|
|
|
use Doctrine\Common\Annotations\Reader;
|
Apply `enum` from Choice Constraints to Items When Choice is Multiple (#1784)
* Apply `enum` from Choice Constraints to Items When Choice is Multiple
Otherwise JSON schema like this is generated:
```
"property": {
"type": "array",
"enum": ["one", "two", "three"],
"items": {
"type": "string"
}
}
```
With this change, however, this schema is generated:
```
"property": {
"type": "array",
"items": {
"type": "string",
"enum": ["one", "two", "three"]
}
}
```
A possible downside here is that the symfony constraint stuff happens
before types are figured out from PHPDoc. So it's _possible_ to end up
with something that won't validated. Take something like this:
```
/**
* @Assert\Choice(multiple=true, choices={"..."})
* @var string
*/
```
This would generate:
```
"property": {
"type": "string",
"items": {
"enum": ["..."]
}
}
```
* Fix CS
* cs
* more cs
* fix tests
Co-authored-by: Guilhem Niot <guilhem@gniot.fr>
2021-02-19 02:41:32 -06:00
|
|
|
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
2020-05-28 13:19:11 +02:00
|
|
|
use OpenApi\Annotations as OA;
|
2021-03-12 00:59:35 +01:00
|
|
|
use Symfony\Component\Validator\Constraint;
|
2018-01-25 14:59:48 +01:00
|
|
|
use Symfony\Component\Validator\Constraints as Assert;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
class SymfonyConstraintAnnotationReader
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @var Reader
|
|
|
|
*/
|
|
|
|
private $annotationsReader;
|
|
|
|
|
|
|
|
/**
|
2020-05-28 13:19:11 +02:00
|
|
|
* @var OA\Schema
|
2018-01-25 14:59:48 +01:00
|
|
|
*/
|
|
|
|
private $schema;
|
|
|
|
|
|
|
|
public function __construct(Reader $annotationsReader)
|
|
|
|
{
|
|
|
|
$this->annotationsReader = $annotationsReader;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the given property and schema with defined Symfony constraints.
|
2021-03-12 00:59:35 +01:00
|
|
|
*
|
|
|
|
* @param \ReflectionProperty|\ReflectionMethod $reflection
|
2018-01-25 14:59:48 +01:00
|
|
|
*/
|
2020-07-12 15:04:20 +02:00
|
|
|
public function updateProperty($reflection, OA\Property $property): void
|
2018-01-25 14:59:48 +01:00
|
|
|
{
|
2021-03-12 00:59:35 +01:00
|
|
|
foreach ($this->getAnnotations($reflection) as $annotation) {
|
2018-01-25 14:59:48 +01:00
|
|
|
if ($annotation instanceof Assert\NotBlank || $annotation instanceof Assert\NotNull) {
|
2020-09-20 20:38:26 +02:00
|
|
|
// To support symfony/validator < 4.3
|
2020-09-28 22:45:24 +03:00
|
|
|
if ($annotation instanceof Assert\NotBlank && \property_exists($annotation, 'allowNull') && $annotation->allowNull) {
|
2020-08-31 23:22:42 +03:00
|
|
|
// The field is optional
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-08-30 00:32:11 +02:00
|
|
|
// The field is required
|
|
|
|
if (null === $this->schema) {
|
|
|
|
continue;
|
2018-01-25 14:59:48 +01:00
|
|
|
}
|
|
|
|
|
2018-09-11 13:42:50 +03:00
|
|
|
$propertyName = $this->getSchemaPropertyName($property);
|
|
|
|
if (null === $propertyName) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
$existingRequiredFields = OA\UNDEFINED !== $this->schema->required ? $this->schema->required : [];
|
2018-09-11 13:42:50 +03:00
|
|
|
$existingRequiredFields[] = $propertyName;
|
2018-08-30 00:32:11 +02:00
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
$this->schema->required = array_values(array_unique($existingRequiredFields));
|
2018-08-30 00:32:11 +02:00
|
|
|
} elseif ($annotation instanceof Assert\Length) {
|
2021-02-10 10:33:55 -06:00
|
|
|
if (isset($annotation->min)) {
|
|
|
|
$property->minLength = (int) $annotation->min;
|
|
|
|
}
|
|
|
|
if (isset($annotation->max)) {
|
|
|
|
$property->maxLength = (int) $annotation->max;
|
|
|
|
}
|
2018-08-30 00:32:11 +02:00
|
|
|
} elseif ($annotation instanceof Assert\Regex) {
|
2018-01-25 14:59:48 +01:00
|
|
|
$this->appendPattern($property, $annotation->getHtmlPattern());
|
2018-08-30 00:32:11 +02:00
|
|
|
} elseif ($annotation instanceof Assert\Count) {
|
2021-05-25 06:26:27 -05:00
|
|
|
if (isset($annotation->min)) {
|
|
|
|
$property->minItems = (int) $annotation->min;
|
|
|
|
}
|
|
|
|
if (isset($annotation->max)) {
|
|
|
|
$property->maxItems = (int) $annotation->max;
|
|
|
|
}
|
2018-08-30 00:32:11 +02:00
|
|
|
} elseif ($annotation instanceof Assert\Choice) {
|
Apply `enum` from Choice Constraints to Items When Choice is Multiple (#1784)
* Apply `enum` from Choice Constraints to Items When Choice is Multiple
Otherwise JSON schema like this is generated:
```
"property": {
"type": "array",
"enum": ["one", "two", "three"],
"items": {
"type": "string"
}
}
```
With this change, however, this schema is generated:
```
"property": {
"type": "array",
"items": {
"type": "string",
"enum": ["one", "two", "three"]
}
}
```
A possible downside here is that the symfony constraint stuff happens
before types are figured out from PHPDoc. So it's _possible_ to end up
with something that won't validated. Take something like this:
```
/**
* @Assert\Choice(multiple=true, choices={"..."})
* @var string
*/
```
This would generate:
```
"property": {
"type": "string",
"items": {
"enum": ["..."]
}
}
```
* Fix CS
* cs
* more cs
* fix tests
Co-authored-by: Guilhem Niot <guilhem@gniot.fr>
2021-02-19 02:41:32 -06:00
|
|
|
$this->applyEnumFromChoiceConstraint($property, $annotation, $reflection);
|
2018-12-19 15:41:27 +01:00
|
|
|
} elseif ($annotation instanceof Assert\Range) {
|
2021-05-25 06:39:21 -05:00
|
|
|
if (isset($annotation->min)) {
|
|
|
|
$property->minimum = (int) $annotation->min;
|
|
|
|
}
|
|
|
|
if (isset($annotation->max)) {
|
|
|
|
$property->maximum = (int) $annotation->max;
|
|
|
|
}
|
2018-12-19 15:41:27 +01:00
|
|
|
} elseif ($annotation instanceof Assert\LessThan) {
|
2020-07-06 19:50:34 +02:00
|
|
|
$property->exclusiveMaximum = true;
|
|
|
|
$property->maximum = (int) $annotation->value;
|
2018-12-19 15:41:27 +01:00
|
|
|
} elseif ($annotation instanceof Assert\LessThanOrEqual) {
|
2020-07-06 19:50:34 +02:00
|
|
|
$property->maximum = (int) $annotation->value;
|
2021-05-07 14:18:44 +02:00
|
|
|
} elseif ($annotation instanceof Assert\GreaterThan) {
|
|
|
|
$property->exclusiveMinimum = true;
|
|
|
|
$property->minimum = (int) $annotation->value;
|
|
|
|
} elseif ($annotation instanceof Assert\GreaterThanOrEqual) {
|
|
|
|
$property->minimum = (int) $annotation->value;
|
2018-01-25 14:59:48 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
public function setSchema($schema): void
|
2018-01-25 14:59:48 +01:00
|
|
|
{
|
|
|
|
$this->schema = $schema;
|
|
|
|
}
|
|
|
|
|
2018-09-11 13:42:50 +03:00
|
|
|
/**
|
|
|
|
* Get assigned property name for property schema.
|
|
|
|
*/
|
2020-05-28 13:19:11 +02:00
|
|
|
private function getSchemaPropertyName(OA\Schema $property): ?string
|
2018-09-11 13:42:50 +03:00
|
|
|
{
|
|
|
|
if (null === $this->schema) {
|
|
|
|
return null;
|
|
|
|
}
|
2020-05-28 13:19:11 +02:00
|
|
|
foreach ($this->schema->properties as $schemaProperty) {
|
2018-09-11 13:42:50 +03:00
|
|
|
if ($schemaProperty === $property) {
|
2020-05-28 13:19:11 +02:00
|
|
|
return OA\UNDEFINED !== $schemaProperty->property ? $schemaProperty->property : null;
|
2018-09-11 13:42:50 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-01-25 14:59:48 +01:00
|
|
|
/**
|
|
|
|
* Append the pattern from the constraint to the existing pattern.
|
|
|
|
*/
|
2020-05-28 13:19:11 +02:00
|
|
|
private function appendPattern(OA\Schema $property, $newPattern): void
|
2018-01-25 14:59:48 +01:00
|
|
|
{
|
2018-01-26 17:09:38 +01:00
|
|
|
if (null === $newPattern) {
|
|
|
|
return;
|
|
|
|
}
|
2020-05-28 13:19:11 +02:00
|
|
|
if (OA\UNDEFINED !== $property->pattern) {
|
|
|
|
$property->pattern = sprintf('%s, %s', $property->pattern, $newPattern);
|
2018-01-25 14:59:48 +01:00
|
|
|
} else {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->pattern = $newPattern;
|
2018-01-25 14:59:48 +01:00
|
|
|
}
|
|
|
|
}
|
Apply `enum` from Choice Constraints to Items When Choice is Multiple (#1784)
* Apply `enum` from Choice Constraints to Items When Choice is Multiple
Otherwise JSON schema like this is generated:
```
"property": {
"type": "array",
"enum": ["one", "two", "three"],
"items": {
"type": "string"
}
}
```
With this change, however, this schema is generated:
```
"property": {
"type": "array",
"items": {
"type": "string",
"enum": ["one", "two", "three"]
}
}
```
A possible downside here is that the symfony constraint stuff happens
before types are figured out from PHPDoc. So it's _possible_ to end up
with something that won't validated. Take something like this:
```
/**
* @Assert\Choice(multiple=true, choices={"..."})
* @var string
*/
```
This would generate:
```
"property": {
"type": "string",
"items": {
"enum": ["..."]
}
}
```
* Fix CS
* cs
* more cs
* fix tests
Co-authored-by: Guilhem Niot <guilhem@gniot.fr>
2021-02-19 02:41:32 -06:00
|
|
|
|
|
|
|
/**
|
2021-03-12 00:59:35 +01:00
|
|
|
* @param \ReflectionProperty|\ReflectionMethod $reflection
|
Apply `enum` from Choice Constraints to Items When Choice is Multiple (#1784)
* Apply `enum` from Choice Constraints to Items When Choice is Multiple
Otherwise JSON schema like this is generated:
```
"property": {
"type": "array",
"enum": ["one", "two", "three"],
"items": {
"type": "string"
}
}
```
With this change, however, this schema is generated:
```
"property": {
"type": "array",
"items": {
"type": "string",
"enum": ["one", "two", "three"]
}
}
```
A possible downside here is that the symfony constraint stuff happens
before types are figured out from PHPDoc. So it's _possible_ to end up
with something that won't validated. Take something like this:
```
/**
* @Assert\Choice(multiple=true, choices={"..."})
* @var string
*/
```
This would generate:
```
"property": {
"type": "string",
"items": {
"enum": ["..."]
}
}
```
* Fix CS
* cs
* more cs
* fix tests
Co-authored-by: Guilhem Niot <guilhem@gniot.fr>
2021-02-19 02:41:32 -06:00
|
|
|
*/
|
|
|
|
private function applyEnumFromChoiceConstraint(OA\Schema $property, Assert\Choice $choice, $reflection): void
|
|
|
|
{
|
|
|
|
if ($choice->callback) {
|
|
|
|
$enumValues = call_user_func(is_array($choice->callback) ? $choice->callback : [$reflection->class, $choice->callback]);
|
|
|
|
} else {
|
|
|
|
$enumValues = $choice->choices;
|
|
|
|
}
|
|
|
|
|
|
|
|
$setEnumOnThis = $property;
|
|
|
|
if ($choice->multiple) {
|
|
|
|
$setEnumOnThis = Util::getChild($property, OA\Items::class);
|
|
|
|
}
|
|
|
|
|
|
|
|
$setEnumOnThis->enum = array_values($enumValues);
|
|
|
|
}
|
2021-03-12 00:59:35 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param \ReflectionProperty|\ReflectionMethod $reflection
|
|
|
|
*/
|
|
|
|
private function getAnnotations($reflection): \Traversable
|
|
|
|
{
|
2021-06-11 07:23:54 +02:00
|
|
|
if (\PHP_VERSION_ID >= 80000 && class_exists(Constraint::class)) {
|
2021-03-12 00:59:35 +01:00
|
|
|
foreach ($reflection->getAttributes(Constraint::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
|
|
|
|
yield $attribute->newInstance();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($reflection instanceof \ReflectionProperty) {
|
|
|
|
yield from $this->annotationsReader->getPropertyAnnotations($reflection);
|
|
|
|
} elseif ($reflection instanceof \ReflectionMethod) {
|
|
|
|
yield from $this->annotationsReader->getMethodAnnotations($reflection);
|
|
|
|
}
|
|
|
|
}
|
2018-01-25 14:59:48 +01:00
|
|
|
}
|