diff --git a/.gitignore b/.gitignore index ae94d3b..289c53c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /.phpunit.result.cache /Tests/Functional/cache /Tests/Functional/logs +.idea diff --git a/Annotation/Areas.php b/Annotation/Areas.php index f4ce2f9..c4bf48f 100644 --- a/Annotation/Areas.php +++ b/Annotation/Areas.php @@ -14,6 +14,7 @@ namespace Nelmio\ApiDocBundle\Annotation; /** * @Annotation */ +#[\Attribute(\Attribute::TARGET_METHOD)] final class Areas { /** @var string[] */ diff --git a/Annotation/Model.php b/Annotation/Model.php index 5a25829..43e7b84 100644 --- a/Annotation/Model.php +++ b/Annotation/Model.php @@ -13,10 +13,12 @@ namespace Nelmio\ApiDocBundle\Annotation; use OpenApi\Annotations\AbstractAnnotation; use OpenApi\Annotations\Parameter; +use OpenApi\Generator; /** * @Annotation */ +#[\Attribute(\Attribute::TARGET_METHOD)] final class Model extends AbstractAnnotation { /** {@inheritdoc} */ @@ -46,4 +48,22 @@ final class Model extends AbstractAnnotation * @var mixed[] */ public $options; + + /** + * @param mixed[] $properties + * @param string[] $groups + * @param mixed[] $options + */ + public function __construct( + array $properties = [], + string $type = Generator::UNDEFINED, + array $groups = null, + array $options = null + ) { + parent::__construct($properties + [ + 'type' => $type, + 'groups' => $groups, + 'options' => $options, + ]); + } } diff --git a/Annotation/Operation.php b/Annotation/Operation.php index 3a8e9f7..9eb075e 100644 --- a/Annotation/Operation.php +++ b/Annotation/Operation.php @@ -16,6 +16,7 @@ use OpenApi\Annotations\Operation as BaseOperation; /** * @Annotation */ +#[\Attribute(\Attribute::TARGET_METHOD)] class Operation extends BaseOperation { } diff --git a/Annotation/Security.php b/Annotation/Security.php index 95bbc4b..8676014 100644 --- a/Annotation/Security.php +++ b/Annotation/Security.php @@ -16,6 +16,7 @@ use OpenApi\Annotations\AbstractAnnotation; /** * @Annotation */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Security extends AbstractAnnotation { /** {@inheritdoc} */ @@ -35,4 +36,15 @@ class Security extends AbstractAnnotation * @var string[] */ public $scopes = []; + + public function __construct( + array $properties = [], + string $name = null, + array $scopes = [] + ) { + parent::__construct($properties + [ + 'name' => $name, + 'scopes' => $scopes, + ]); + } } diff --git a/Describer/OpenApiPhpDescriber.php b/Describer/OpenApiPhpDescriber.php index 3c3e485..1445da4 100644 --- a/Describer/OpenApiPhpDescriber.php +++ b/Describer/OpenApiPhpDescriber.php @@ -67,12 +67,14 @@ final class OpenApiPhpDescriber $classAnnotations = array_filter($this->annotationReader->getClassAnnotations($declaringClass), function ($v) { return $v instanceof OA\AbstractAnnotation; }); + $classAnnotations = array_merge($classAnnotations, $this->getAttributesAsAnnotation($declaringClass, OA\AbstractAnnotation::class)); $classAnnotations[$declaringClass->getName()] = $classAnnotations; } $annotations = array_filter($this->annotationReader->getMethodAnnotations($method), function ($v) { return $v instanceof OA\AbstractAnnotation; }); + $annotations = array_merge($annotations, $this->getAttributesAsAnnotation($method, OA\AbstractAnnotation::class)); if (0 === count($annotations) && 0 === count($classAnnotations[$declaringClass->getName()])) { continue; @@ -190,4 +192,23 @@ final class OpenApiPhpDescriber return $path; } + + /** + * @param \ReflectionClass|\ReflectionMethod $reflection + * + * @return OA\AbstractAnnotation[] + */ + private function getAttributesAsAnnotation($reflection, string $className): array + { + $annotations = []; + if (\PHP_VERSION_ID < 80100) { + return $annotations; + } + + foreach ($reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $annotations[] = $attribute->newInstance(); + } + + return $annotations; + } } diff --git a/ModelDescriber/Annotations/OpenApiAnnotationsReader.php b/ModelDescriber/Annotations/OpenApiAnnotationsReader.php index a7d20f1..3325fb6 100644 --- a/ModelDescriber/Annotations/OpenApiAnnotationsReader.php +++ b/ModelDescriber/Annotations/OpenApiAnnotationsReader.php @@ -39,8 +39,8 @@ class OpenApiAnnotationsReader public function updateSchema(\ReflectionClass $reflectionClass, OA\Schema $schema): void { - /** @var OA\Schema $oaSchema */ - if (!$oaSchema = $this->annotationsReader->getClassAnnotation($reflectionClass, OA\Schema::class)) { + /** @var OA\Schema|null $oaSchema */ + if (!$oaSchema = $this->getAnnotation($reflectionClass, OA\Schema::class)) { return; } @@ -56,10 +56,8 @@ class OpenApiAnnotationsReader public function getPropertyName($reflection, string $default): string { - /** @var OA\Property $oaProperty */ - if ($reflection instanceof \ReflectionProperty && !$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflection, OA\Property::class)) { - return $default; - } elseif ($reflection instanceof \ReflectionMethod && !$oaProperty = $this->annotationsReader->getMethodAnnotation($reflection, OA\Property::class)) { + /** @var OA\Property|null $oaProperty */ + if (!$oaProperty = $this->getAnnotation($reflection, OA\Property::class)) { return $default; } @@ -78,10 +76,8 @@ class OpenApiAnnotationsReader 'filename' => $declaringClass->getFileName(), ])); - /** @var OA\Property $oaProperty */ - if ($reflection instanceof \ReflectionProperty && !$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflection, OA\Property::class)) { - return; - } elseif ($reflection instanceof \ReflectionMethod && !$oaProperty = $this->annotationsReader->getMethodAnnotation($reflection, OA\Property::class)) { + /** @var OA\Property|null $oaProperty */ + if (!$oaProperty = $this->getAnnotation($reflection, OA\Property::class)) { return; } $this->setContext(null); @@ -95,4 +91,28 @@ class OpenApiAnnotationsReader $property->mergeProperties($oaProperty); } + + /** + * @param \ReflectionClass|\ReflectionProperty|\ReflectionMethod $reflection + * + * @return mixed + */ + private function getAnnotation($reflection, string $className) + { + if (\PHP_VERSION_ID >= 80100) { + if (null !== $attribute = $reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) { + return $attribute->newInstance(); + } + } + + if ($reflection instanceof \ReflectionClass) { + return $this->annotationsReader->getClassAnnotation($reflection, $className); + } elseif ($reflection instanceof \ReflectionProperty) { + return $this->annotationsReader->getPropertyAnnotation($reflection, $className); + } elseif ($reflection instanceof \ReflectionMethod) { + return $this->annotationsReader->getMethodAnnotation($reflection, $className); + } + + return null; + } } diff --git a/ModelDescriber/ObjectModelDescriber.php b/ModelDescriber/ObjectModelDescriber.php index cab10c1..5f20cd7 100644 --- a/ModelDescriber/ObjectModelDescriber.php +++ b/ModelDescriber/ObjectModelDescriber.php @@ -72,7 +72,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes); $annotationsReader->updateDefinition($reflClass, $schema); - $discriminatorMap = $this->doctrineReader->getClassAnnotation($reflClass, DiscriminatorMap::class); + $discriminatorMap = $this->getAnnotation($reflClass, DiscriminatorMap::class); if ($discriminatorMap && Generator::UNDEFINED === $schema->discriminator) { $this->applyOpenApiDiscriminator( $model, @@ -183,6 +183,20 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar throw new \Exception(sprintf('Type "%s" is not supported in %s::$%s. You may use the `@OA\Property(type="")` annotation to specify it manually.', $types[0]->getBuiltinType(), $model->getType()->getClassName(), $propertyName)); } + /** + * @return mixed + */ + private function getAnnotation(\ReflectionClass $reflection, string $className) + { + if (\PHP_VERSION_ID >= 80000) { + if (null !== $attribute = $reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) { + return $attribute->newInstance(); + } + } + + return $this->doctrineReader->getClassAnnotation($reflection, $className); + } + public function supports(Model $model): bool { return Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType() && class_exists($model->getType()->getClassName()); diff --git a/RouteDescriber/FosRestDescriber.php b/RouteDescriber/FosRestDescriber.php index 7235f5d..c63249a 100644 --- a/RouteDescriber/FosRestDescriber.php +++ b/RouteDescriber/FosRestDescriber.php @@ -44,6 +44,8 @@ final class FosRestDescriber implements RouteDescriberInterface $annotations = array_filter($annotations, static function ($value) { return $value instanceof RequestParam || $value instanceof QueryParam; }); + $annotations = array_merge($annotations, $this->getAttributesAsAnnotation($reflectionMethod, RequestParam::class)); + $annotations = array_merge($annotations, $this->getAttributesAsAnnotation($reflectionMethod, QueryParam::class)); foreach ($this->getOperations($api, $route) as $operation) { foreach ($annotations as $annotation) { @@ -185,4 +187,21 @@ final class FosRestDescriber implements RouteDescriberInterface $schema->format = $format; } } + + /** + * @return OA\AbstractAnnotation[] + */ + private function getAttributesAsAnnotation(\ReflectionMethod $reflection, string $className): array + { + $annotations = []; + if (\PHP_VERSION_ID < 80100) { + return $annotations; + } + + foreach ($reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $annotations[] = $attribute->newInstance(); + } + + return $annotations; + } } diff --git a/Routing/FilteredRouteCollectionBuilder.php b/Routing/FilteredRouteCollectionBuilder.php index 4c9b14b..db79f71 100644 --- a/Routing/FilteredRouteCollectionBuilder.php +++ b/Routing/FilteredRouteCollectionBuilder.php @@ -130,13 +130,23 @@ final class FilteredRouteCollectionBuilder } /** @var Areas|null $areas */ - $areas = $this->annotationReader->getMethodAnnotation( - $reflectionMethod, - Areas::class - ); + $areas = $this->getAttributesAsAnnotation($reflectionMethod, Areas::class)[0] ?? null; if (null === $areas) { - $areas = $this->annotationReader->getClassAnnotation($reflectionMethod->getDeclaringClass(), Areas::class); + /** @var Areas|null $areas */ + $areas = $this->getAttributesAsAnnotation($reflectionMethod->getDeclaringClass(), Areas::class)[0] ?? null; + + if (null === $areas) { + /** @var Areas|null $areas */ + $areas = $this->annotationReader->getMethodAnnotation( + $reflectionMethod, + Areas::class + ); + + if (null === $areas) { + $areas = $this->annotationReader->getClassAnnotation($reflectionMethod->getDeclaringClass(), Areas::class); + } + } } return (null !== $areas) ? $areas->has($this->area) : false; @@ -168,4 +178,23 @@ final class FilteredRouteCollectionBuilder return false; } + + /** + * @param \ReflectionClass|\ReflectionMethod $reflection + * + * @return Areas[] + */ + private function getAttributesAsAnnotation($reflection, string $className): array + { + $annotations = []; + if (\PHP_VERSION_ID < 80100) { + return $annotations; + } + + foreach ($reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $annotations[] = $attribute->newInstance(); + } + + return $annotations; + } } diff --git a/Tests/Functional/Controller/ApiController.php b/Tests/Functional/Controller/ApiController.php index 69372d3..8985a50 100644 --- a/Tests/Functional/Controller/ApiController.php +++ b/Tests/Functional/Controller/ApiController.php @@ -11,244 +11,20 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller; -use Nelmio\ApiDocBundle\Annotation\Areas; -use Nelmio\ApiDocBundle\Annotation\Model; -use Nelmio\ApiDocBundle\Annotation\Operation; -use Nelmio\ApiDocBundle\Annotation\Security; -use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article; -use Nelmio\ApiDocBundle\Tests\Functional\Entity\CompoundEntity; -use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraints; -use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyDiscriminator; -use Nelmio\ApiDocBundle\Tests\Functional\Entity\User; -use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType; -use Nelmio\ApiDocBundle\Tests\Functional\Form\UserType; -use OpenApi\Annotations as OA; use Symfony\Component\Routing\Annotation\Route; -/** - * @Route("/api", name="api_", host="api.example.com") - */ -class ApiController -{ +if (\PHP_VERSION_ID >= 80100) { /** - * @OA\Get( - * @OA\Response( - * response="200", - * description="Success", - * @Model(type=Article::class, groups={"light"})) - * ) - * ) - * @OA\Parameter(ref="#/components/parameters/test") - * @Route("/article/{id}", methods={"GET"}) - * @OA\Parameter(name="Accept-Version", in="header", @OA\Schema(type="string")) - * @OA\Parameter(name="Application-Name", in="header", @OA\Schema(type="string")) + * @Route("/api", name="api_", host="api.example.com") */ - public function fetchArticleAction() + class ApiController extends ApiController81 { } - +} else { /** - * The method LINK is not supported by OpenAPI so the method will be ignored. - * - * @Route("/swagger", methods={"GET", "LINK"}) - * @Route("/swagger2", methods={"GET"}) - * @Operation( - * @OA\Response(response="201", description="An example resource") - * ) - * @OA\Get( - * path="/api/swagger2", - * @OA\Parameter(name="Accept-Version", in="header", @OA\Schema(type="string")) - * ) - * @OA\Post( - * path="/api/swagger2", - * @OA\Response(response="203", description="but 203 is not actually allowed (wrong method)") - * ) + * @Route("/api", name="api_", host="api.example.com") */ - public function swaggerAction() - { - } - - /** - * @Route("/swagger/implicit", methods={"GET", "POST"}) - * @OA\Response( - * response="201", - * description="Operation automatically detected", - * @Model(type=User::class) - * ), - * @OA\RequestBody( - * description="This is a request body", - * @OA\JsonContent( - * type="array", - * @OA\Items(ref=@Model(type=User::class)) - * ) - * ) - * @OA\Tag(name="implicit") - */ - public function implicitSwaggerAction() - { - } - - /** - * @Route("/test/users/{user}", methods={"POST"}, schemes={"https"}, requirements={"user"="/foo/"}) - * @OA\Response( - * response="201", - * description="Operation automatically detected", - * @Model(type=User::class) - * ), - * @OA\RequestBody( - * description="This is a request body", - * @Model(type=UserType::class, options={"bar": "baz"})) - * ) - */ - public function submitUserTypeAction() - { - } - - /** - * @Route("/test/{user}", methods={"GET"}, schemes={"https"}, requirements={"user"="/foo/"}) - * @OA\Response(response=200, description="sucessful") - */ - public function userAction() - { - } - - /** - * This action is deprecated. - * - * Please do not use this action. - * - * @Route("/deprecated", methods={"GET"}) - * - * @deprecated - */ - public function deprecatedAction() - { - } - - /** - * This action is not documented. It is excluded by the config. - * - * @Route("/admin", methods={"GET"}) - */ - public function adminAction() - { - } - - /** - * @OA\Get( - * path="/filtered", - * @OA\Response(response="201", description="") - * ) - */ - public function filteredAction() - { - } - - /** - * @Route("/form", methods={"POST"}) - * @OA\RequestBody( - * description="Request content", - * @Model(type=DummyType::class)) - * ) - * @OA\Response(response="201", description="") - */ - public function formAction() - { - } - - /** - * @Route("/security") - * @OA\Response(response="201", description="") - * @Security(name="api_key") - * @Security(name="basic") - * @Security(name="oauth2", scopes={"scope_1"}) - */ - public function securityAction() - { - } - - /** - * @Route("/swagger/symfonyConstraints", methods={"GET"}) - * @OA\Response( - * response="201", - * description="Used for symfony constraints test", - * @Model(type=SymfonyConstraints::class) - * ) - */ - public function symfonyConstraintsAction() - { - } - - /** - * @OA\Response( - * response="200", - * description="Success", - * ref="#/components/schemas/Test" - * ), - * @OA\Response( - * response="201", - * ref="#/components/responses/201" - * ) - * @Route("/configReference", methods={"GET"}) - */ - public function configReferenceAction() - { - } - - /** - * @Route("/multi-annotations", methods={"GET", "POST"}) - * @OA\Get(description="This is the get operation") - * @OA\Post(description="This is post") - * - * @OA\Response(response=200, description="Worked well!", @Model(type=DummyType::class)) - */ - public function operationsWithOtherAnnotations() - { - } - - /** - * @Route("/areas/new", methods={"GET", "POST"}) - * - * @Areas({"area", "area2"}) - */ - public function newAreaAction() - { - } - - /** - * @Route("/compound", methods={"GET", "POST"}) - * - * @OA\Response(response=200, description="Worked well!", @Model(type=CompoundEntity::class)) - */ - public function compoundEntityAction() - { - } - - /** - * @Route("/discriminator-mapping", methods={"GET", "POST"}) - * - * @OA\Response(response=200, description="Worked well!", @Model(type=SymfonyDiscriminator::class)) - */ - public function discriminatorMappingAction() - { - } - - /** - * @Route("/named_route-operation-id", name="named_route_operation_id", methods={"GET", "POST"}) - * - * @OA\Response(response=200, description="success") - */ - public function namedRouteOperationIdAction() - { - } - - /** - * @Route("/custom-operation-id", methods={"GET", "POST"}) - * - * @Operation(operationId="custom-operation-id") - * @OA\Response(response=200, description="success") - */ - public function customOperationIdAction() + class ApiController extends ApiController80 { } } diff --git a/Tests/Functional/Controller/ApiController80.php b/Tests/Functional/Controller/ApiController80.php new file mode 100644 index 0000000..64b4101 --- /dev/null +++ b/Tests/Functional/Controller/ApiController80.php @@ -0,0 +1,251 @@ + new OA\Response( + response: '200', + description: 'Success', + properties: [ + 'value' => new Model(type: Article::class, groups: ['light']), + ], + ), + ])] + #[OA\Parameter(ref: '#/components/parameters/test')] + #[Route('/article_attributes/{id}', methods: ['GET'])] + #[OA\Parameter(name: 'Accept-Version', in: 'header', properties: ['value' => new OA\Schema(type: 'string')])] + public function fetchArticleActionWithAttributes() + { + } +} diff --git a/Tests/Functional/Controller/FOSRestController.php b/Tests/Functional/Controller/FOSRestController.php index 772af46..699198d 100644 --- a/Tests/Functional/Controller/FOSRestController.php +++ b/Tests/Functional/Controller/FOSRestController.php @@ -11,30 +11,20 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller; -use FOS\RestBundle\Controller\Annotations\QueryParam; -use FOS\RestBundle\Controller\Annotations\RequestParam; use Symfony\Component\Routing\Annotation\Route; -use Symfony\Component\Validator\Constraints\DateTime; -use Symfony\Component\Validator\Constraints\IsTrue; -use Symfony\Component\Validator\Constraints\Regex; -/** - * @Route("/api", host="api.example.com") - */ -class FOSRestController -{ +if (\PHP_VERSION_ID >= 80100) { /** - * @Route("/fosrest.{_format}", methods={"POST"}) - * @QueryParam(name="foo", requirements=@Regex("/^\d+$/")) - * @QueryParam(name="mapped", map=true) - * @RequestParam(name="Barraa", key="bar", requirements="\d+") - * @RequestParam(name="baz", requirements=@IsTrue) - * @RequestParam(name="datetime", requirements=@DateTime("Y-m-d\TH:i:sP")) - * @RequestParam(name="datetimeAlt", requirements=@DateTime("c")) - * @RequestParam(name="datetimeNoFormat", requirements=@DateTime()) - * @RequestParam(name="date", requirements=@DateTime("Y-m-d")) + * @Route("/api", host="api.example.com") */ - public function fosrestAction() + class FOSRestController extends FOSRestController81 + { + } +} else { + /** + * @Route("/api", host="api.example.com") + */ + class FOSRestController extends FOSRestController80 { } } diff --git a/Tests/Functional/Controller/FOSRestController80.php b/Tests/Functional/Controller/FOSRestController80.php new file mode 100644 index 0000000..1e12df9 --- /dev/null +++ b/Tests/Functional/Controller/FOSRestController80.php @@ -0,0 +1,37 @@ + 'api.example.com']); } - public function testFOSRestAction() + /** + * @dataProvider provideRoute + */ + public function testFOSRestAction(string $route) { - $operation = $this->getOperation('/api/fosrest', 'post'); + $operation = $this->getOperation($route, 'post'); $this->assertHasParameter('foo', 'query', $operation); $this->assertInstanceOf(OA\RequestBody::class, $operation->requestBody); @@ -66,4 +69,13 @@ class FOSRestTest extends WebTestCase // The _format path attribute should be removed $this->assertNotHasParameter('_format', 'path', $operation); } + + public function provideRoute(): iterable + { + yield 'Annotations' => ['/api/fosrest']; + + if (\PHP_VERSION_ID >= 80100) { + yield 'Attributes' => ['/api/fosrest_attributes']; + } + } } diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php index 0704bc6..b2a42ef 100644 --- a/Tests/Functional/FunctionalTest.php +++ b/Tests/Functional/FunctionalTest.php @@ -40,9 +40,12 @@ class FunctionalTest extends WebTestCase $this->assertNotHasPath('/api/admin', $api); } - public function testFetchArticleAction() + /** + * @dataProvider provideArticleRoute + */ + public function testFetchArticleAction(string $articleRoute) { - $operation = $this->getOperation('/api/article/{id}', 'get'); + $operation = $this->getOperation($articleRoute, 'get'); $this->assertHasResponse('200', $operation); $response = $this->getOperationResponse($operation, '200'); @@ -56,6 +59,15 @@ class FunctionalTest extends WebTestCase $this->assertNotHasProperty('author', Util::getProperty($articleModel, 'author')); } + public function provideArticleRoute(): iterable + { + yield 'Annotations' => ['/api/article/{id}']; + + if (\PHP_VERSION_ID >= 80100) { + yield 'Attributes' => ['/api/article_attributes/{id}']; + } + } + public function testFilteredAction() { $openApi = $this->getOpenApiDefinition(); diff --git a/Tests/ModelDescriber/Annotations/AnnotationReaderTest.php b/Tests/ModelDescriber/Annotations/AnnotationReaderTest.php new file mode 100644 index 0000000..1b94351 --- /dev/null +++ b/Tests/ModelDescriber/Annotations/AnnotationReaderTest.php @@ -0,0 +1,67 @@ +merge([new OA\Property(['property' => 'property1'])]); + $schema->merge([new OA\Property(['property' => 'property2'])]); + + $registry = new ModelRegistry([], new OA\OpenApi([]), []); + $symfonyConstraintAnnotationReader = new OpenApiAnnotationsReader(new AnnotationReader(), $registry, ['json']); + $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); + $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property2'), $schema->properties[1]); + + $this->assertEquals($schema->properties[0]->example, 1); + $this->assertEquals($schema->properties[0]->description, Generator::UNDEFINED); + + $this->assertEquals($schema->properties[1]->example, 'some example'); + $this->assertEquals($schema->properties[1]->description, 'some description'); + } + + public function provideProperty(): iterable + { + yield 'Annotations' => [new class() { + /** + * @OA\Property(example=1) + */ + private $property1; + /** + * @OA\Property(example="some example", description="some description") + */ + private $property2; + }]; + + if (\PHP_VERSION_ID >= 80100) { + yield 'Attributes' => [new class() { + #[OA\Property(example: 1)] + private $property1; + #[OA\Property(example: 'some example', description: 'some description')] + private $property2; + }]; + } + } +}