Skip to content

Commit ce64341

Browse files
bnowakBartłomiej NowakDjordyKoert
authored
feat: Support for generic types (#2503)
## Description It adds support for generating api schema of generic types. ## What type of PR is this? (check all applicable) - [ ] Bug Fix - [x] Feature - [ ] Refactor - [ ] Deprecation - [ ] Breaking Change - [ ] Documentation Update - [ ] CI ## Checklist - [ ] I have made corresponding changes to the documentation (`docs/`) - [x] I have made corresponding changes to the changelog (`CHANGELOG.md`) --------- Co-authored-by: Bartłomiej Nowak <[email protected]> Co-authored-by: Djordy Koert <[email protected]> Co-authored-by: djordy <[email protected]>
1 parent 752cde9 commit ce64341

File tree

9 files changed

+596
-0
lines changed

9 files changed

+596
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 5.5.0
44
* Schemas deduplication now compare generated schemas to reduce automatically named schemas (Entity / Entity2 / Entity3 / ...).
5+
* Added support for generic types describing
56

67
## 5.3.0
78
Added support for Symfony's `TranslatableInterface`

config/services.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,14 @@
197197
<tag name="nelmio_api_doc.type_describer" priority="-1000" />
198198
</service>
199199

200+
<service id="nelmio_api_doc.type_describer.generic_class" class="Nelmio\ApiDocBundle\TypeDescriber\GenericClassDescriber" public="false">
201+
<tag name="nelmio_api_doc.type_describer" priority="-1000" />
202+
</service>
203+
204+
<service id="nelmio_api_doc.type_describer.generic_collection" class="Nelmio\ApiDocBundle\TypeDescriber\GenericCollectionDescriber" public="false">
205+
<tag name="nelmio_api_doc.type_describer" priority="-1000" />
206+
</service>
207+
200208
<service id="nelmio_api_doc.type_describer.integer" class="Nelmio\ApiDocBundle\TypeDescriber\IntegerDescriber" public="false">
201209
<tag name="nelmio_api_doc.type_describer" priority="-1000" />
202210
</service>
@@ -225,6 +233,12 @@
225233
<tag name="nelmio_api_doc.type_describer" priority="-1000" />
226234
</service>
227235

236+
<service id="nelmio_api_doc.type_describer.template" class="Nelmio\ApiDocBundle\TypeDescriber\TemplateDescriber" public="false">
237+
<argument type="service" id="nelmio_api_doc.type_describer.chain" />
238+
239+
<tag name="nelmio_api_doc.type_describer" priority="-1000" />
240+
</service>
241+
228242
<service id="nelmio_api_doc.type_describer.union" class="Nelmio\ApiDocBundle\TypeDescriber\UnionDescriber" public="false">
229243
<tag name="nelmio_api_doc.type_describer" priority="-1000" />
230244
</service>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the NelmioApiDocBundle package.
7+
*
8+
* (c) Nelmio
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Nelmio\ApiDocBundle\TypeDescriber;
15+
16+
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
17+
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
18+
use Nelmio\ApiDocBundle\Model\Model;
19+
use OpenApi\Annotations\Schema;
20+
use phpDocumentor\Reflection\DocBlock\Tags\Template;
21+
use phpDocumentor\Reflection\DocBlockFactory;
22+
use phpDocumentor\Reflection\DocBlockFactoryInterface;
23+
use Symfony\Component\PropertyInfo\Type as LegacyType;
24+
use Symfony\Component\TypeInfo\Type;
25+
use Symfony\Component\TypeInfo\Type\GenericType;
26+
use Symfony\Component\TypeInfo\Type\ObjectType;
27+
28+
/**
29+
* @implements TypeDescriberInterface<GenericType>
30+
*
31+
* @internal
32+
*/
33+
final class GenericClassDescriber implements TypeDescriberInterface, ModelRegistryAwareInterface
34+
{
35+
use ModelRegistryAwareTrait;
36+
37+
private DocBlockFactoryInterface $docBlockFactory;
38+
39+
public function __construct()
40+
{
41+
$this->docBlockFactory = DocBlockFactory::createInstance();
42+
}
43+
44+
public function describe(Type $type, Schema $schema, array $context = []): void
45+
{
46+
$wrappedType = $type->getWrappedType();
47+
$reflectionClass = new \ReflectionClass($wrappedType->getClassName());
48+
49+
if (false !== $reflectionClass->getDocComment()) {
50+
/** @var Template[] $templateTags */
51+
$templateTags = $this->docBlockFactory
52+
->create($reflectionClass)
53+
->getTagsByName('template');
54+
$templateNames = array_map(
55+
static fn (Template $template): string => $template->getTemplateName(),
56+
$templateTags,
57+
);
58+
59+
if ([] !== $templateNames) {
60+
$context[TemplateDescriber::TEMPLATES_KEY] = array_combine($templateNames, $type->getVariableTypes());
61+
}
62+
}
63+
64+
$schema->ref = $this->modelRegistry->register(
65+
new Model(new LegacyType('object', false, $wrappedType->getClassName()), serializationContext: $context)
66+
);
67+
}
68+
69+
public function supports(Type $type, array $context = []): bool
70+
{
71+
return $type instanceof GenericType
72+
&& $type->getWrappedType() instanceof ObjectType;
73+
}
74+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the NelmioApiDocBundle package.
7+
*
8+
* (c) Nelmio
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Nelmio\ApiDocBundle\TypeDescriber;
15+
16+
use OpenApi\Annotations\Schema;
17+
use Symfony\Component\TypeInfo\Type;
18+
use Symfony\Component\TypeInfo\Type\CollectionType;
19+
use Symfony\Component\TypeInfo\Type\TemplateType;
20+
21+
/**
22+
* @implements TypeDescriberInterface<CollectionType>
23+
*
24+
* @internal
25+
*/
26+
final class GenericCollectionDescriber implements TypeDescriberInterface, TypeDescriberAwareInterface
27+
{
28+
use TypeDescriberAwareTrait;
29+
30+
public function describe(Type $type, Schema $schema, array $context = []): void
31+
{
32+
if (!$type->getCollectionKeyType() instanceof TemplateType) {
33+
throw new \LogicException('This describer only supports '.CollectionType::class.' with '.TemplateType::class.' as key type.');
34+
}
35+
36+
$templateTypes = $context[TemplateDescriber::TEMPLATES_KEY];
37+
unset($context[TemplateDescriber::TEMPLATES_KEY]);
38+
39+
if (\array_key_exists($type->getCollectionKeyType()->getName(), $templateTypes)) {
40+
$resolvedKeyType = $templateTypes[$type->getCollectionKeyType()->getName()];
41+
$valueTemplateName = $type->getCollectionValueType() instanceof TemplateType
42+
? $type->getCollectionValueType()->getName()
43+
: null;
44+
$resolvedValueType = $templateTypes[$valueTemplateName] ?? $type->getCollectionValueType();
45+
46+
$collectionType = Type::array($resolvedValueType, $resolvedKeyType, $type->isList());
47+
48+
$this->describer->describe($collectionType, $schema, $context);
49+
}
50+
}
51+
52+
public function supports(Type $type, array $context = []): bool
53+
{
54+
return $type instanceof CollectionType
55+
&& $type->getCollectionKeyType() instanceof TemplateType
56+
&& \array_key_exists(TemplateDescriber::TEMPLATES_KEY, $context);
57+
}
58+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the NelmioApiDocBundle package.
7+
*
8+
* (c) Nelmio
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Nelmio\ApiDocBundle\TypeDescriber;
15+
16+
use OpenApi\Annotations\Schema;
17+
use Symfony\Component\TypeInfo\Type;
18+
use Symfony\Component\TypeInfo\Type\TemplateType;
19+
20+
/**
21+
* @implements TypeDescriberInterface<TemplateType>
22+
*
23+
* @internal
24+
*/
25+
final class TemplateDescriber implements TypeDescriberInterface, TypeDescriberAwareInterface
26+
{
27+
use TypeDescriberAwareTrait;
28+
29+
public const TEMPLATES_KEY = '_nelmio_template_types';
30+
31+
public function describe(Type $type, Schema $schema, array $context = []): void
32+
{
33+
$templateTypes = $context[self::TEMPLATES_KEY];
34+
unset($context[self::TEMPLATES_KEY]);
35+
36+
if (\array_key_exists($type->getName(), $templateTypes)) {
37+
$resolvedType = $templateTypes[$type->getName()];
38+
39+
$this->describer->describe($resolvedType, $schema, $context);
40+
}
41+
}
42+
43+
public function supports(Type $type, array $context = []): bool
44+
{
45+
return $type instanceof TemplateType
46+
&& \array_key_exists(self::TEMPLATES_KEY, $context);
47+
}
48+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the NelmioApiDocBundle package.
7+
*
8+
* (c) Nelmio
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
15+
16+
use Nelmio\ApiDocBundle\Attribute\Model;
17+
use Nelmio\ApiDocBundle\Tests\Functional\Entity\GenericTypes;
18+
use OpenApi\Attributes as OA;
19+
use Symfony\Component\Routing\Attribute\Route;
20+
21+
class GenericTypesController
22+
{
23+
#[OA\Response(
24+
response: '200',
25+
description: 'Success',
26+
content: new Model(type: GenericTypes::class),
27+
)]
28+
#[Route('/generic-types', methods: ['GET'])]
29+
public function genericTypesAction()
30+
{
31+
}
32+
}

tests/Functional/ControllerTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,19 @@ function (RoutingConfigurator $routes) {
658658
->methods(['GET']);
659659
},
660660
];
661+
662+
if (version_compare(Kernel::VERSION, '7.2.0', '>=')) {
663+
yield 'Generic types' => [
664+
'GenericTypesController',
665+
null,
666+
[],
667+
[
668+
'nelmio_api_doc' => [
669+
'type_info' => true,
670+
],
671+
],
672+
];
673+
}
661674
}
662675

663676
private static function getFixture(string $fixture): string
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the NelmioApiDocBundle package.
7+
*
8+
* (c) Nelmio
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
15+
16+
/**
17+
* @template K of array-key
18+
* @template V
19+
*/
20+
class Collection
21+
{
22+
/** @var array<K, V> */
23+
public array $map;
24+
25+
/** @var array<V> */
26+
public array $array;
27+
28+
/** @var list<V> */
29+
public array $list;
30+
}
31+
32+
/**
33+
* @template T
34+
*/
35+
class GenericClass
36+
{
37+
/** @var T */
38+
public mixed $genericProperty;
39+
}
40+
41+
class RegularClass
42+
{
43+
public string $stringProperty;
44+
public int $integerProperty;
45+
}
46+
47+
class GenericTypes
48+
{
49+
/** @var GenericClass<string> */
50+
public GenericClass $string; // GenericClass
51+
/** @var GenericClass<string> */
52+
public GenericClass $string2; // GenericClass
53+
/** @var GenericClass<int> */
54+
public GenericClass $integer; // GenericClass2
55+
56+
/** @var GenericClass<GenericClass<int>> */
57+
public GenericClass $genericClass; // GenericClass3
58+
/** @var GenericClass<RegularClass> */
59+
public GenericClass $regularClass; // GenericClass4
60+
61+
/** @var GenericClass<list<string>> */
62+
public GenericClass $stringList; // GenericClass5
63+
/** @var GenericClass<list<int>> */
64+
public GenericClass $integerList; // GenericClass6
65+
66+
/** @var Collection<string, string> */
67+
public Collection $stringStringCollection; // Collection
68+
/** @var Collection<string, string> */
69+
public Collection $stringStringCollection2; // Collection
70+
/** @var Collection<int, int> */
71+
public Collection $integerIntegerCollection; // Collection2
72+
/** @var Collection<int, RegularClass> */
73+
public Collection $integerRegularClassCollection; // Collection3
74+
}

0 commit comments

Comments
 (0)