Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#2212): support experimental type info component #2349

Draft
wants to merge 28 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
915ae64
Add type-info
DjordyKoert Oct 4, 2024
99c86c4
Implement experimental type-info describers
DjordyKoert Oct 4, 2024
95dfbe9
Register services for experimental type-info describers
DjordyKoert Oct 4, 2024
4c68692
Also check instanceof type
DjordyKoert Oct 4, 2024
58ddc64
Implement experimental behaviour to ObjectModelDescriber
DjordyKoert Oct 4, 2024
2c97df7
Implement class property describers as model describers
DjordyKoert Oct 4, 2024
41d5c1c
Fix union & intersection using wrong property
DjordyKoert Oct 4, 2024
c462cbe
Implement free-form object describer
DjordyKoert Oct 4, 2024
d538707
Implement enum describer
DjordyKoert Oct 4, 2024
99f1491
Fix union & intersection generating invalid spec
DjordyKoert Oct 4, 2024
b8c68c7
Remove enum describer
DjordyKoert Oct 4, 2024
d41df52
Remove unnecessary result class
DjordyKoert Oct 4, 2024
f1b4bef
Deprecate RequiredPropertyDescriber
DjordyKoert Oct 8, 2024
29bc887
Update tests for new required property behaviour
DjordyKoert Oct 8, 2024
074bf30
Use getVersion method
DjordyKoert Oct 8, 2024
9d61909
Remove type-info component (comes with symfony/property-info)
DjordyKoert Oct 8, 2024
0f00b18
Merge branch 'master' into 2212-type-info
DjordyKoert Oct 8, 2024
7eaa240
bump phpstan symfony version
DjordyKoert Oct 8, 2024
f19b9b5
Merge branch 'master' into 2212-type-info
DjordyKoert Oct 17, 2024
b37214f
style fix
DjordyKoert Oct 17, 2024
6c76df2
remove phpstan ignore
DjordyKoert Oct 18, 2024
183eeff
baseline update
DjordyKoert Oct 18, 2024
54599b8
Model new model describers
DjordyKoert Oct 18, 2024
48df032
Rename to TypeDescriber
DjordyKoert Oct 18, 2024
7c045d7
Mark experimental describers as internal
DjordyKoert Oct 18, 2024
79b1bdc
Remove deprecation warning
DjordyKoert Oct 18, 2024
223954d
Remove call to markRequiredProperties
DjordyKoert Oct 18, 2024
0327105
fix invalid context being passed
DjordyKoert Oct 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ jobs:
- name: Setup dependencies
uses: ./.github/workflows/common/composer-install
with:
symfony-version: "7.0.*"
symfony-version: "7.1.*"
install-doctrine-annotations: false

- name: Run PHPStan
Expand Down
55 changes: 55 additions & 0 deletions config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,61 @@
<service id="nelmio_api_doc.swagger.processor.nullable_property" class="Nelmio\ApiDocBundle\Processor\NullablePropertyProcessor">
<tag name="nelmio_api_doc.swagger.processor" />
</service>

<!-- Experimental schema describers (symfony/type-info) -->
<service id="nelmio_api_doc.schema_describer.chain" class="Nelmio\ApiDocBundle\TypeDescriber\ChainDescriber" public="false">
<argument type="tagged" tag="nelmio_api_doc.schema_describer" />

<tag name="nelmio_api_doc.schema_describer" priority="100" />
</service>

<service id="nelmio_api_doc.schema_describer.bool" class="Nelmio\ApiDocBundle\TypeDescriber\BoolDescriber" public="false">
<tag name="nelmio_api_doc.schema_describer" />
</service>

<service id="nelmio_api_doc.schema_describer.dictionary" class="Nelmio\ApiDocBundle\TypeDescriber\DictionaryDescriber" public="false">
<tag name="nelmio_api_doc.schema_describer" />
</service>

<service id="nelmio_api_doc.schema_describer.float" class="Nelmio\ApiDocBundle\TypeDescriber\FloatDescriber" public="false">
<tag name="nelmio_api_doc.schema_describer" />
</service>

<service id="nelmio_api_doc.schema_describer.integer" class="Nelmio\ApiDocBundle\TypeDescriber\IntegerDescriber" public="false">
<tag name="nelmio_api_doc.schema_describer" />
</service>

<service id="nelmio_api_doc.schema_describer.intersection" class="Nelmio\ApiDocBundle\TypeDescriber\IntersectionDescriber" public="false">
<tag name="nelmio_api_doc.schema_describer" />
</service>

<service id="nelmio_api_doc.schema_describer.list" class="Nelmio\ApiDocBundle\TypeDescriber\ListDescriber" public="false">
<tag name="nelmio_api_doc.schema_describer" />
</service>

<service id="nelmio_api_doc.schema_describer.mixed" class="Nelmio\ApiDocBundle\TypeDescriber\MixedDescriber" public="false">
<tag name="nelmio_api_doc.schema_describer" />
</service>

<service id="nelmio_api_doc.schema_describer.nullable" class="Nelmio\ApiDocBundle\TypeDescriber\NullableDescriber" public="false">
<tag name="nelmio_api_doc.schema_describer" />
</service>

<service id="nelmio_api_doc.schema_describer.object_class" class="Nelmio\ApiDocBundle\TypeDescriber\ObjectClassDescriber" public="false">
<tag name="nelmio_api_doc.schema_describer" />
</service>

<service id="nelmio_api_doc.schema_describer.object" class="Nelmio\ApiDocBundle\TypeDescriber\ObjectDescriber" public="false">
<tag name="nelmio_api_doc.schema_describer" />
</service>

<service id="nelmio_api_doc.schema_describer.string" class="Nelmio\ApiDocBundle\TypeDescriber\StringDescriber" public="false">
<tag name="nelmio_api_doc.schema_describer" />
</service>

<service id="nelmio_api_doc.schema_describer.union" class="Nelmio\ApiDocBundle\TypeDescriber\UnionDescriber" public="false">
<tag name="nelmio_api_doc.schema_describer" />
</service>
</services>

</container>
5 changes: 5 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ parameters:
count: 1
path: src/Describer/ExternalDocDescriber.php

-
message: "#^Call to function method_exists\\(\\) with Symfony\\\\Component\\\\PropertyInfo\\\\PropertyInfoExtractorInterface and 'getType' will always evaluate to true\\.$#"
count: 1
path: src/ModelDescriber/ObjectModelDescriber.php

-
message: "#^Method Nelmio\\\\ApiDocBundle\\\\PropertyDescriber\\\\PropertyDescriberInterface\\:\\:describe\\(\\) invoked with 5 parameters, 2\\-3 required\\.$#"
count: 1
Expand Down
4 changes: 4 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public function getConfigTreeBuilder(): TreeBuilder

$rootNode
->children()
->booleanNode('experimental_type_info')
->info('Use the symfony/type-info component for determining types. This is experimental and could be changed at any time without prior notice.')
->defaultFalse()
->end()
->booleanNode('use_validation_groups')
->info('If true, `groups` passed to @Model annotations will be used to limit validation constraints')
->defaultFalse()
Expand Down
5 changes: 5 additions & 0 deletions src/DependencyInjection/NelmioApiDocExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ public function load(array $configs, ContainerBuilder $container): void
array_map(function ($area) { return new Reference(sprintf('nelmio_api_doc.generator.%s', $area)); }, array_keys($config['areas']))
));

if (true === $config['experimental_type_info']) {
$container->getDefinition('nelmio_api_doc.model_describers.object')
->setArgument(2, new Reference('nelmio_api_doc.schema_describer.chain'));
}

$container->getDefinition('nelmio_api_doc.model_describers.object')
->setArgument(3, $config['media_types']);

Expand Down
45 changes: 35 additions & 10 deletions src/ModelDescriber/ObjectModelDescriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use Nelmio\ApiDocBundle\PropertyDescriber\PropertyDescriberInterface;
use Nelmio\ApiDocBundle\TypeDescriber\TypeDescriberInterface;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
Expand All @@ -34,7 +35,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
private PropertyInfoExtractorInterface $propertyInfo;
private ?ClassMetadataFactoryInterface $classMetadataFactory;
private ?Reader $doctrineReader;
/** @var PropertyDescriberInterface|PropertyDescriberInterface[] */
/** @var PropertyDescriberInterface|PropertyDescriberInterface[]|TypeDescriberInterface */
private $propertyDescriber;
/** @var string[] */
private array $mediaTypes;
Expand All @@ -43,9 +44,9 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
private bool $useValidationGroups;

/**
* @param PropertyDescriberInterface|PropertyDescriberInterface[] $propertyDescribers
* @param (NameConverterInterface&AdvancedNameConverterInterface)|null $nameConverter
* @param string[] $mediaTypes
* @param PropertyDescriberInterface|PropertyDescriberInterface[]|TypeDescriberInterface $propertyDescribers
* @param (NameConverterInterface&AdvancedNameConverterInterface)|null $nameConverter
* @param string[] $mediaTypes
*/
public function __construct(
PropertyInfoExtractorInterface $propertyInfo,
Expand All @@ -59,7 +60,7 @@ public function __construct(
if (is_iterable($propertyDescribers)) {
trigger_deprecation('nelmio/api-doc-bundle', '4.17', 'Passing an array of PropertyDescriberInterface to %s() is deprecated. Pass a single PropertyDescriberInterface instead.', __METHOD__);
} else {
if (!$propertyDescribers instanceof PropertyDescriberInterface) {
if (!$propertyDescribers instanceof PropertyDescriberInterface && !$propertyDescribers instanceof TypeDescriberInterface) {
throw new \InvalidArgumentException(sprintf('Argument 3 passed to %s() must be an array of %s or a single %s.', __METHOD__, PropertyDescriberInterface::class, PropertyDescriberInterface::class));
}
}
Expand Down Expand Up @@ -175,12 +176,36 @@ public function describe(Model $model, OA\Schema $schema)
continue;
}

$types = $this->propertyInfo->getTypes($class, $propertyName);
if (null === $types || 0 === count($types)) {
throw new \LogicException(sprintf('The PropertyInfo component was not able to guess the type of %s::$%s. You may need to add a `@var` annotation or use `@OA\Property(type="")` to make its type explicit.', $class, $propertyName));
/*
* @experimental
*/
if ($this->propertyDescriber instanceof TypeDescriberInterface) {
if (false === method_exists($this->propertyInfo, 'getType')) {
throw new \RuntimeException('The PropertyInfo component is missing the "getType" method. Are you running on version 7.1?');
}

$type = $this->propertyInfo->getType($class, $propertyName);
if (null === $type) {
throw new \LogicException(sprintf('The PropertyInfo component was not able to guess the type of %s::$%s. You may need to add a `@var` annotation or use `@OA\Property(type="")` to make its type explicit.', $class, $propertyName));
}

if ($this->propertyDescriber instanceof ModelRegistryAwareInterface) {
$this->propertyDescriber->setModelRegistry($this->modelRegistry);
}

if (!$this->propertyDescriber->supports($type, $model->getSerializationContext())) {
throw new \Exception(sprintf('Type "%s" $model->getSerializationContext() not supported in %s::$%s. You may use the `@OA\Property(type="")` annotation to specify it manually.', $type->__toString(), $model->getType()->getClassName(), $propertyName));
}

$this->propertyDescriber->describe($type, $property, $model->getSerializationContext());
} else {
$types = $this->propertyInfo->getTypes($class, $propertyName);
if (null === $types || 0 === count($types)) {
throw new \LogicException(sprintf('The PropertyInfo component was not able to guess the type of %s::$%s. You may need to add a `@var` annotation or use `@OA\Property(type="")` to make its type explicit.', $class, $propertyName));
}

$this->describeProperty($types, $model, $property, $propertyName, $schema);
}

$this->describeProperty($types, $model, $property, $propertyName, $schema);
}

$this->markRequiredProperties($schema);
Expand Down
1 change: 1 addition & 0 deletions src/PropertyDescriber/RequiredPropertyDescriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Nelmio\ApiDocBundle\PropertyDescriber;

use Nelmio\ApiDocBundle\ModelDescriber\ObjectModelDescriber;
use OpenApi\Annotations as OA;
use OpenApi\Generator;

Expand Down
37 changes: 37 additions & 0 deletions src/TypeDescriber/BoolDescriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?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\TypeDescriber;

use OpenApi\Annotations\Schema;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\TypeIdentifier;

/**
* @implements TypeDescriberInterface<Type\BuiltinType>
*
* @experimental
*
* @internal
*/
final class BoolDescriber implements TypeDescriberInterface
{
public function describe(Type $type, Schema $schema, array $context = []): void
{
$schema->type = 'boolean';
}

public function supports(Type $type, array $context = []): bool
{
return $type instanceof Type\BuiltinType
&& $type->isA(TypeIdentifier::BOOL);
}
}
68 changes: 68 additions & 0 deletions src/TypeDescriber/ChainDescriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?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\TypeDescriber;

use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use OpenApi\Annotations\Schema;
use Symfony\Component\TypeInfo\Type;

/**
* @implements TypeDescriberInterface<Type>
*
* @experimental
*
* @internal
*/
final class ChainDescriber implements TypeDescriberInterface, ModelRegistryAwareInterface
{
use ModelRegistryAwareTrait;

/** @var iterable<TypeDescriberInterface> */
private iterable $describers;

/**
* @param iterable<TypeDescriberInterface> $describers
*/
public function __construct(
iterable $describers
) {
$this->describers = $describers;
}

public function describe(Type $type, Schema $schema, array $context = []): void
{
foreach ($this->describers as $describer) {
/* BC layer for Symfony < 6.3 @see https://symfony.com/doc/6.3/service_container/tags.html#reference-tagged-services */
if ($describer instanceof self) {
continue;
}

if ($describer instanceof ModelRegistryAwareInterface) {
$describer->setModelRegistry($this->modelRegistry);
}

if ($describer instanceof TypeDescriberAwareInterface) {
$describer->setDescriber($this);
}

if ($describer->supports($type, $context)) {
$describer->describe($type, $schema, $context);
}
}
}

public function supports(Type $type, array $context = []): bool
{
return true;
}
}
45 changes: 45 additions & 0 deletions src/TypeDescriber/DictionaryDescriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?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\TypeDescriber;

use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use OpenApi\Annotations\Schema;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\TypeIdentifier;

/**
* @implements TypeDescriberInterface<CollectionType>
*
* @experimental
*
* @internal
*/
final class DictionaryDescriber implements TypeDescriberInterface, TypeDescriberAwareInterface
{
use TypeDescriberAwareTrait;

public function describe(Type $type, Schema $schema, array $context = []): void
{
$schema->type = 'object';
$additionalProperties = Util::getChild($schema, OA\AdditionalProperties::class);

$this->describer->describe($type->getCollectionValueType(), $additionalProperties, $context);
}

public function supports(Type $type, array $context = []): bool
{
return $type instanceof CollectionType
&& $type->getCollectionKeyType()->isA(TypeIdentifier::STRING);
}
}
38 changes: 38 additions & 0 deletions src/TypeDescriber/FloatDescriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?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\TypeDescriber;

use OpenApi\Annotations\Schema;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\TypeIdentifier;

/**
* @implements TypeDescriberInterface<Type\BuiltinType>
*
* @experimental
*
* @internal
*/
final class FloatDescriber implements TypeDescriberInterface
{
public function describe(Type $type, Schema $schema, array $context = []): void
{
$schema->type = 'number';
$schema->format = 'float';
}

public function supports(Type $type, array $context = []): bool
{
return $type instanceof Type\BuiltinType
&& $type->isA(TypeIdentifier::FLOAT);
}
}
Loading
Loading