Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f1ecc30

Browse files
authored
feat(openapi): add backed enum support (#5120)
1 parent 114b31e commit f1ecc30

15 files changed

Lines changed: 221 additions & 82 deletions

File tree

features/openapi/docs.feature

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Feature: Documentation support
3636
And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_put" exists
3737
And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_read" exists
3838
And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_write" exists
39+
And the OpenAPI class "Person" exists
3940
And the OpenAPI class "RelatedDummy" exists
4041
And the OpenAPI class "NoCollectionDummy" exists
4142
And the OpenAPI class "RelatedToDummyFriend" exists
@@ -57,6 +58,21 @@ Feature: Documentation support
5758
# Properties
5859
And the "id" property exists for the OpenAPI class "Dummy"
5960
And the "name" property is required for the OpenAPI class "Dummy"
61+
And the "genderType" property exists for the OpenAPI class "Person"
62+
And the "genderType" property for the OpenAPI class "Person" should be equal to:
63+
"""
64+
{
65+
"default": "male",
66+
"example": "male",
67+
"type": "string",
68+
"enum": [
69+
"male",
70+
"female",
71+
null
72+
],
73+
"nullable": true
74+
}
75+
"""
6076
# Enable these tests when SF 4.4 / PHP 7.1 support is dropped
6177
#And the "isDummyBoolean" property exists for the OpenAPI class "DummyBoolean"
6278
#And the "isDummyBoolean" property is not read only for the OpenAPI class "DummyBoolean"

phpstan.neon.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,5 @@ parameters:
8383
-
8484
message: '#^Property .+ is unused.$#'
8585
path: tests/Doctrine/Odm/PropertyInfo/Fixtures/DoctrineDummy.php
86+
# Waiting for https://github.com/laminas/laminas-code/pull/150
87+
- '#Call to an undefined method ReflectionEnum::.+#'

src/Doctrine/Odm/PropertyInfo/DoctrineExtractor.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ public function getTypes($class, $property, array $context = []): ?array
9292
if ($metadata->hasField($property)) {
9393
$typeOfField = $metadata->getTypeOfField($property);
9494
$nullable = $metadata instanceof MongoDbClassMetadata && $metadata->isNullable($property);
95+
$enumType = null;
96+
if (null !== $enumClass = $metadata instanceof MongoDbClassMetadata ? $metadata->getFieldMapping($property)['enumType'] ?? null : null) {
97+
$enumType = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $enumClass);
98+
}
9599

96100
switch ($typeOfField) {
97101
case MongoDbType::DATE:
@@ -102,11 +106,16 @@ public function getTypes($class, $property, array $context = []): ?array
102106
return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)];
103107
case MongoDbType::COLLECTION:
104108
return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT))];
105-
default:
106-
$builtinType = $this->getPhpType($typeOfField);
107-
108-
return $builtinType ? [new Type($builtinType, $nullable)] : null;
109+
case MongoDbType::INT:
110+
case MongoDbType::STRING:
111+
if ($enumType) {
112+
return [$enumType];
113+
}
109114
}
115+
116+
$builtinType = $this->getPhpType($typeOfField);
117+
118+
return $builtinType ? [new Type($builtinType, $nullable)] : null;
110119
}
111120

112121
return null;

src/JsonSchema/SchemaFactory.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
175175
}
176176

177177
if (!isset($propertySchema['default']) && !empty($default = $propertyMetadata->getDefault())) {
178+
if ($default instanceof \BackedEnum) {
179+
$default = $default->value;
180+
}
178181
$propertySchema['default'] = $default;
179182
}
180183

src/JsonSchema/TypeFactory.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,15 @@ private function makeBasicType(Type $type, string $format = 'json', ?bool $reada
7272
Type::BUILTIN_TYPE_INT => ['type' => 'integer'],
7373
Type::BUILTIN_TYPE_FLOAT => ['type' => 'number'],
7474
Type::BUILTIN_TYPE_BOOL => ['type' => 'boolean'],
75-
Type::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $format, $readableLink, $serializerContext, $schema),
75+
Type::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $type->isNullable(), $format, $readableLink, $serializerContext, $schema),
7676
default => ['type' => 'string'],
7777
};
7878
}
7979

8080
/**
8181
* Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided.
8282
*/
83-
private function getClassType(?string $className, string $format, ?bool $readableLink, ?array $serializerContext, ?Schema $schema): array
83+
private function getClassType(?string $className, bool $nullable, string $format, ?bool $readableLink, ?array $serializerContext, ?Schema $schema): array
8484
{
8585
if (null === $className) {
8686
return ['type' => 'string'];
@@ -116,6 +116,18 @@ private function getClassType(?string $className, string $format, ?bool $readabl
116116
'format' => 'binary',
117117
];
118118
}
119+
if (is_a($className, \BackedEnum::class, true)) {
120+
$rEnum = new \ReflectionEnum($className);
121+
$enumCases = array_map(static fn (\ReflectionEnumBackedCase $rCase) => $rCase->getBackingValue(), $rEnum->getCases());
122+
if ($nullable) {
123+
$enumCases[] = null;
124+
}
125+
126+
return [
127+
'type' => (string) $rEnum->getBackingType(),
128+
'enum' => $enumCases,
129+
];
130+
}
119131

120132
// Skip if $schema is null (filters only support basic types)
121133
if (null === $schema) {

tests/Behat/OpenApiContext.php

Lines changed: 22 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
use Behat\Behat\Context\Context;
1717
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
1818
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
19+
use Behat\Gherkin\Node\PyStringNode;
1920
use Behatch\Context\RestContext;
21+
use Behatch\Json\Json;
2022
use PHPUnit\Framework\Assert;
2123
use PHPUnit\Framework\ExpectationFailedException;
2224

@@ -42,51 +44,25 @@ public function gatherContexts(BeforeScenarioScope $scope): void
4244
$this->restContext = $restContext;
4345
}
4446

45-
/**
46-
* @Then the Swagger class :class exists
47-
*/
48-
public function assertTheSwaggerClassExist(string $className): void
49-
{
50-
try {
51-
$this->getClassInfo($className);
52-
} catch (\InvalidArgumentException $e) {
53-
throw new ExpectationFailedException(sprintf('The class "%s" doesn\'t exist.', $className), null, $e);
54-
}
55-
}
56-
5747
/**
5848
* @Then the OpenAPI class :class exists
5949
*/
6050
public function assertTheOpenApiClassExist(string $className): void
6151
{
6252
try {
63-
$this->getClassInfo($className, 3);
53+
$this->getClassInfo($className);
6454
} catch (\InvalidArgumentException $e) {
6555
throw new ExpectationFailedException(sprintf('The class "%s" doesn\'t exist.', $className), null, $e);
6656
}
6757
}
6858

69-
/**
70-
* @Then the Swagger class :class doesn't exist
71-
*/
72-
public function assertTheSwaggerClassNotExist(string $className): void
73-
{
74-
try {
75-
$this->getClassInfo($className);
76-
} catch (\InvalidArgumentException) {
77-
return;
78-
}
79-
80-
throw new ExpectationFailedException(sprintf('The class "%s" exists.', $className));
81-
}
82-
8359
/**
8460
* @Then the OpenAPI class :class doesn't exist
8561
*/
8662
public function assertTheOpenAPIClassNotExist(string $className): void
8763
{
8864
try {
89-
$this->getClassInfo($className, 3);
65+
$this->getClassInfo($className);
9066
} catch (\InvalidArgumentException) {
9167
return;
9268
}
@@ -95,7 +71,6 @@ public function assertTheOpenAPIClassNotExist(string $className): void
9571
}
9672

9773
/**
98-
* @Then the Swagger path :arg1 exists
9974
* @Then the OpenAPI path :arg1 exists
10075
*/
10176
public function assertThePathExist(string $path): void
@@ -105,54 +80,32 @@ public function assertThePathExist(string $path): void
10580
Assert::assertTrue(isset($json->paths) && isset($json->paths->{$path}));
10681
}
10782

108-
/**
109-
* @Then the :prop property exists for the Swagger class :class
110-
*/
111-
public function assertThePropertyExistForTheSwaggerClass(string $propertyName, string $className): void
112-
{
113-
try {
114-
$this->getPropertyInfo($propertyName, $className);
115-
} catch (\InvalidArgumentException $e) {
116-
throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" doesn\'t exist.', $propertyName, $className), null, $e);
117-
}
118-
}
119-
12083
/**
12184
* @Then the :prop property exists for the OpenAPI class :class
12285
*/
12386
public function assertThePropertyExistForTheOpenApiClass(string $propertyName, string $className): void
12487
{
12588
try {
126-
$this->getPropertyInfo($propertyName, $className, 3);
89+
$this->getPropertyInfo($propertyName, $className);
12790
} catch (\InvalidArgumentException $e) {
12891
throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" doesn\'t exist.', $propertyName, $className), null, $e);
12992
}
13093
}
13194

132-
/**
133-
* @Then the :prop property is required for the Swagger class :class
134-
*/
135-
public function assertThePropertyIsRequiredForTheSwaggerClass(string $propertyName, string $className): void
136-
{
137-
if (!\in_array($propertyName, $this->getClassInfo($className)->required, true)) {
138-
throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" should be required', $propertyName, $className));
139-
}
140-
}
141-
14295
/**
14396
* @Then the :prop property is required for the OpenAPI class :class
14497
*/
14598
public function assertThePropertyIsRequiredForTheOpenAPIClass(string $propertyName, string $className): void
14699
{
147-
if (!\in_array($propertyName, $this->getClassInfo($className, 3)->required, true)) {
100+
if (!\in_array($propertyName, $this->getClassInfo($className)->required, true)) {
148101
throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" should be required', $propertyName, $className));
149102
}
150103
}
151104

152105
/**
153-
* @Then the :prop property is not read only for the Swagger class :class
106+
* @Then the :prop property is not read only for the OpenAPI class :class
154107
*/
155-
public function assertThePropertyIsNotReadOnlyForTheSwaggerClass(string $propertyName, string $className): void
108+
public function assertThePropertyIsNotReadOnlyForTheOpenAPIClass(string $propertyName, string $className): void
156109
{
157110
$propertyInfo = $this->getPropertyInfo($propertyName, $className);
158111
if (property_exists($propertyInfo, 'readOnly') && $propertyInfo->readOnly) {
@@ -161,13 +114,15 @@ public function assertThePropertyIsNotReadOnlyForTheSwaggerClass(string $propert
161114
}
162115

163116
/**
164-
* @Then the :prop property is not read only for the OpenAPI class :class
117+
* @Then the :prop property for the OpenAPI class :class should be equal to:
165118
*/
166-
public function assertThePropertyIsNotReadOnlyForTheOpenAPIClass(string $propertyName, string $className): void
119+
public function assertThePropertyForTheOpenAPIClassShouldBeEqualTo(string $propertyName, string $className, PyStringNode $propertyContent): void
167120
{
168-
$propertyInfo = $this->getPropertyInfo($propertyName, $className, 3);
169-
if (property_exists($propertyInfo, 'readOnly') && $propertyInfo->readOnly) {
170-
throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" should not be read only', $propertyName, $className));
121+
$propertyInfo = $this->getPropertyInfo($propertyName, $className);
122+
$propertyInfoJson = new Json(json_encode($propertyInfo));
123+
124+
if (new Json($propertyContent) != $propertyInfoJson) {
125+
throw new ExpectationFailedException(sprintf("Property \"%s\" of class \"%s\" is '%s'", $propertyName, $className, $propertyInfoJson));
171126
}
172127
}
173128

@@ -176,12 +131,10 @@ public function assertThePropertyIsNotReadOnlyForTheOpenAPIClass(string $propert
176131
*
177132
* @throws \InvalidArgumentException
178133
*/
179-
private function getPropertyInfo(string $propertyName, string $className, int $specVersion = 2): \stdClass
134+
private function getPropertyInfo(string $propertyName, string $className): \stdClass
180135
{
181-
/**
182-
* @var iterable $properties
183-
*/
184-
$properties = $this->getProperties($className, $specVersion);
136+
/** @var iterable $properties */
137+
$properties = $this->getProperties($className);
185138
foreach ($properties as $classPropertyName => $property) {
186139
if ($classPropertyName === $propertyName) {
187140
return $property;
@@ -194,19 +147,19 @@ private function getPropertyInfo(string $propertyName, string $className, int $s
194147
/**
195148
* Gets all operations of a given class.
196149
*/
197-
private function getProperties(string $className, int $specVersion = 2): \stdClass
150+
private function getProperties(string $className): \stdClass
198151
{
199-
return $this->getClassInfo($className, $specVersion)->{'properties'} ?? new \stdClass();
152+
return $this->getClassInfo($className)->{'properties'} ?? new \stdClass();
200153
}
201154

202155
/**
203156
* Gets information about a class.
204157
*
205158
* @throws \InvalidArgumentException
206159
*/
207-
private function getClassInfo(string $className, int $specVersion = 2): \stdClass
160+
private function getClassInfo(string $className): \stdClass
208161
{
209-
$nodes = 2 === $specVersion ? $this->getLastJsonResponse()->{'definitions'} : $this->getLastJsonResponse()->{'components'}->{'schemas'};
162+
$nodes = $this->getLastJsonResponse()->{'components'}->{'schemas'};
210163
foreach ($nodes as $classTitle => $classData) {
211164
if ($classTitle === $className) {
212165
return $classData;

tests/Doctrine/Odm/PropertyInfo/DoctrineExtractorTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
use ApiPlatform\Test\DoctrineMongoDbOdmSetup;
1818
use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineDummy;
1919
use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineEmbeddable;
20+
use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineEnum;
2021
use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineFooType;
2122
use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineGeneratedValue;
2223
use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineRelation;
2324
use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineWithEmbedded;
25+
use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\EnumInt;
26+
use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\EnumString;
2427
use Doctrine\Common\Collections\Collection;
2528
use Doctrine\ODM\MongoDB\DocumentManager;
2629
use Doctrine\ODM\MongoDB\Types\Type as MongoDbType;
@@ -128,6 +131,13 @@ public function testExtractWithEmbedMany(): void
128131
$this->assertEquals($expectedTypes, $actualTypes);
129132
}
130133

134+
public function testExtractEnum(): void
135+
{
136+
$this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumString::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumString'));
137+
$this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumInt::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumInt'));
138+
$this->assertNull($this->createExtractor()->getTypes(DoctrineEnum::class, 'enumCustom'));
139+
}
140+
131141
public function typesProvider(): array
132142
{
133143
return [
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures;
15+
16+
use Doctrine\ODM\MongoDB\Mapping\Annotations\Document;
17+
use Doctrine\ODM\MongoDB\Mapping\Annotations\Field;
18+
use Doctrine\ODM\MongoDB\Mapping\Annotations\Id;
19+
20+
/**
21+
* @author Alan Poulain <[email protected]>
22+
*/
23+
#[Document]
24+
class DoctrineEnum
25+
{
26+
#[Id]
27+
public int $id;
28+
29+
#[Field(enumType: EnumString::class)]
30+
protected EnumString $enumString;
31+
32+
#[Field(type: 'int', enumType: EnumInt::class)]
33+
protected EnumInt $enumInt;
34+
35+
#[Field(type: 'custom_foo', enumType: EnumInt::class)]
36+
protected EnumInt $enumCustom;
37+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures;
15+
16+
enum EnumInt: int
17+
{
18+
case Foo = 0;
19+
case Bar = 1;
20+
}

0 commit comments

Comments
 (0)