From de32660ba43f7dd5f47a7a38d33d5d2c1c367d65 Mon Sep 17 00:00:00 2001 From: Martin Rademacher Date: Sun, 26 Feb 2023 11:37:20 +1300 Subject: [PATCH] Allow FQCN in `$ref` (#1418) --- Examples/using-links-php81/PullRequest.php | 4 +-- phpstan-baseline.neon | 5 ---- src/Analysers/AttributeAnnotationFactory.php | 2 +- src/Annotations/Examples.php | 2 +- src/Annotations/Header.php | 2 +- src/Annotations/Link.php | 2 +- src/Annotations/Parameter.php | 2 +- src/Annotations/PathItem.php | 2 +- src/Annotations/RequestBody.php | 2 +- src/Annotations/Response.php | 2 +- src/Annotations/Schema.php | 2 +- src/Annotations/SecurityScheme.php | 2 +- src/Attributes/AdditionalProperties.php | 1 + src/Attributes/Examples.php | 5 ++-- src/Attributes/Header.php | 5 ++-- src/Attributes/Items.php | 1 + src/Attributes/JsonContent.php | 1 + src/Attributes/Link.php | 7 +++-- src/Attributes/ParameterTrait.php | 1 + src/Attributes/Property.php | 1 + src/Attributes/RequestBody.php | 1 + src/Attributes/Response.php | 1 + src/Attributes/Schema.php | 1 + src/Attributes/SecurityScheme.php | 7 +++-- src/Attributes/XmlContent.php | 1 + src/Processors/AugmentProperties.php | 8 +----- src/Processors/AugmentRefs.php | 29 ++++++++++++++++++-- src/Processors/Concerns/RefTrait.php | 24 ++++++++++++++++ src/Processors/ExpandEnums.php | 5 ++-- tests/Fixtures/Apis/Attributes/basic.php | 6 ++-- tests/Fixtures/Apis/Mixed/basic.php | 4 +-- tests/Fixtures/Scratch/ClassRef.php | 29 ++++++++++++++++++++ tests/Fixtures/Scratch/ClassRef.yaml | 17 ++++++++++++ 33 files changed, 139 insertions(+), 45 deletions(-) create mode 100644 src/Processors/Concerns/RefTrait.php create mode 100644 tests/Fixtures/Scratch/ClassRef.php create mode 100644 tests/Fixtures/Scratch/ClassRef.yaml diff --git a/Examples/using-links-php81/PullRequest.php b/Examples/using-links-php81/PullRequest.php index 8be45dc7d..aca919c25 100644 --- a/Examples/using-links-php81/PullRequest.php +++ b/Examples/using-links-php81/PullRequest.php @@ -27,12 +27,12 @@ public function __construct( /** * @var Repository */ - #[OAT\Property(ref: '#/components/schemas/repository')] + #[OAT\Property(ref: Repository::class)] public $repository; /** * @var User */ - #[OAT\Property(ref: '#/components/schemas/user')] + #[OAT\Property(ref: User::class)] public $author; } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3051fef90..015662f41 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -50,11 +50,6 @@ parameters: count: 1 path: src/Generator.php - - - message: "#^Instanceof between \\(callable\\)\\|OpenApi\\\\Processors\\\\ProcessorInterface and class\\-string\\|false results in an error\\.$#" - count: 1 - path: src/Generator.php - - message: "#^Variable \\$logLine in empty\\(\\) always exists and is not falsy\\.$#" count: 1 diff --git a/src/Analysers/AttributeAnnotationFactory.php b/src/Analysers/AttributeAnnotationFactory.php index 7e7e733f5..31b7f5e53 100644 --- a/src/Analysers/AttributeAnnotationFactory.php +++ b/src/Analysers/AttributeAnnotationFactory.php @@ -93,7 +93,7 @@ public function build(\Reflector $reflector, Context $context): array } $annotations = array_values(array_filter($annotations, function ($a) { - return $a !== null && $a instanceof OA\AbstractAnnotation; + return $a instanceof OA\AbstractAnnotation; })); // merge backwards into parents... diff --git a/src/Annotations/Examples.php b/src/Annotations/Examples.php index c38a24ba0..fc26cc9f2 100644 --- a/src/Annotations/Examples.php +++ b/src/Annotations/Examples.php @@ -16,7 +16,7 @@ class Examples extends AbstractAnnotation /** * @see [Using refs](https://swagger.io/docs/specification/using-ref/) * - * @var string|object + * @var string|class-string|object */ public $ref = Generator::UNDEFINED; diff --git a/src/Annotations/Header.php b/src/Annotations/Header.php index 38b8f028e..385488b52 100644 --- a/src/Annotations/Header.php +++ b/src/Annotations/Header.php @@ -17,7 +17,7 @@ class Header extends AbstractAnnotation /** * @see [Using refs](https://swagger.io/docs/specification/using-ref/) * - * @var string|object + * @var string|class-string|object */ public $ref = Generator::UNDEFINED; diff --git a/src/Annotations/Link.php b/src/Annotations/Link.php index b224a5584..a4bec3bfa 100644 --- a/src/Annotations/Link.php +++ b/src/Annotations/Link.php @@ -29,7 +29,7 @@ class Link extends AbstractAnnotation /** * @see [Using refs](https://swagger.io/docs/specification/using-ref/) * - * @var string|object + * @var string|class-string|object */ public $ref = Generator::UNDEFINED; diff --git a/src/Annotations/Parameter.php b/src/Annotations/Parameter.php index 95eb80b39..9ace7f4f3 100644 --- a/src/Annotations/Parameter.php +++ b/src/Annotations/Parameter.php @@ -22,7 +22,7 @@ class Parameter extends AbstractAnnotation /** * @see [Using refs](https://swagger.io/docs/specification/using-ref/) * - * @var string|object + * @var string|class-string|object */ public $ref = Generator::UNDEFINED; diff --git a/src/Annotations/PathItem.php b/src/Annotations/PathItem.php index 214d563b0..aa5f6da85 100644 --- a/src/Annotations/PathItem.php +++ b/src/Annotations/PathItem.php @@ -23,7 +23,7 @@ class PathItem extends AbstractAnnotation /** * @see [Using refs](https://swagger.io/docs/specification/using-ref/) * - * @var string|object + * @var string|class-string|object */ public $ref = Generator::UNDEFINED; diff --git a/src/Annotations/RequestBody.php b/src/Annotations/RequestBody.php index 347771138..83d08fbbb 100644 --- a/src/Annotations/RequestBody.php +++ b/src/Annotations/RequestBody.php @@ -20,7 +20,7 @@ class RequestBody extends AbstractAnnotation /** * @see [Using refs](https://swagger.io/docs/specification/using-ref/) * - * @var string|object + * @var string|class-string|object */ public $ref = Generator::UNDEFINED; diff --git a/src/Annotations/Response.php b/src/Annotations/Response.php index 73d91bce5..e0182e19c 100644 --- a/src/Annotations/Response.php +++ b/src/Annotations/Response.php @@ -21,7 +21,7 @@ class Response extends AbstractAnnotation /** * @see [Using refs](https://swagger.io/docs/specification/using-ref/) * - * @var string|object + * @var string|class-string|object */ public $ref = Generator::UNDEFINED; diff --git a/src/Annotations/Schema.php b/src/Annotations/Schema.php index fc3c90832..8b51db386 100644 --- a/src/Annotations/Schema.php +++ b/src/Annotations/Schema.php @@ -26,7 +26,7 @@ class Schema extends AbstractAnnotation /** * @see [Using refs](https://swagger.io/docs/specification/using-ref/) * - * @var string|object + * @var string|class-string|object */ public $ref = Generator::UNDEFINED; diff --git a/src/Annotations/SecurityScheme.php b/src/Annotations/SecurityScheme.php index 6564e1f4c..785024025 100644 --- a/src/Annotations/SecurityScheme.php +++ b/src/Annotations/SecurityScheme.php @@ -18,7 +18,7 @@ class SecurityScheme extends AbstractAnnotation /** * @see [Using refs](https://swagger.io/docs/specification/using-ref/) * - * @var string|object + * @var string|class-string|object */ public $ref = Generator::UNDEFINED; diff --git a/src/Attributes/AdditionalProperties.php b/src/Attributes/AdditionalProperties.php index 90d0f8138..d71d40459 100644 --- a/src/Attributes/AdditionalProperties.php +++ b/src/Attributes/AdditionalProperties.php @@ -12,6 +12,7 @@ class AdditionalProperties extends \OpenApi\Annotations\AdditionalProperties { /** + * @param string|class-string|object|null $ref * @param string[] $required * @param Property[] $properties * @param int|float $maximum diff --git a/src/Attributes/Examples.php b/src/Attributes/Examples.php index 820e2f4ea..fd936bd5a 100644 --- a/src/Attributes/Examples.php +++ b/src/Attributes/Examples.php @@ -12,8 +12,9 @@ class Examples extends \OpenApi\Annotations\Examples { /** - * @param array|null $x - * @param Attachable[]|null $attachables + * @param string|class-string|object|null $ref + * @param array|null $x + * @param Attachable[]|null $attachables */ public function __construct( ?string $example = null, diff --git a/src/Attributes/Header.php b/src/Attributes/Header.php index 9c83b6b85..4404aaffc 100644 --- a/src/Attributes/Header.php +++ b/src/Attributes/Header.php @@ -11,8 +11,9 @@ class Header extends \OpenApi\Annotations\Header { /** - * @param array|null $x - * @param Attachable[]|null $attachables + * @param string|class-string|object|null $ref + * @param array|null $x + * @param Attachable[]|null $attachables */ public function __construct( string|object|null $ref = null, diff --git a/src/Attributes/Items.php b/src/Attributes/Items.php index a153e6827..9786a9b87 100644 --- a/src/Attributes/Items.php +++ b/src/Attributes/Items.php @@ -12,6 +12,7 @@ class Items extends \OpenApi\Annotations\Items { /** + * @param string|class-string|object|null $ref * @param string[] $required * @param Property[] $properties * @param int|float $maximum diff --git a/src/Attributes/JsonContent.php b/src/Attributes/JsonContent.php index 20a4d5e2b..cf2b8a1b9 100644 --- a/src/Attributes/JsonContent.php +++ b/src/Attributes/JsonContent.php @@ -12,6 +12,7 @@ class JsonContent extends \OpenApi\Annotations\JsonContent { /** + * @param string|class-string|object|null $ref * @param array $examples * @param string[] $required * @param Property[] $properties diff --git a/src/Attributes/Link.php b/src/Attributes/Link.php index 4f90659a1..c26600b05 100644 --- a/src/Attributes/Link.php +++ b/src/Attributes/Link.php @@ -12,9 +12,10 @@ class Link extends \OpenApi\Annotations\Link { /** - * @param array $parameters - * @param array|null $x - * @param Attachable[]|null $attachables + * @param string|class-string|object|null $ref + * @param array $parameters + * @param array|null $x + * @param Attachable[]|null $attachables */ public function __construct( ?string $link = null, diff --git a/src/Attributes/ParameterTrait.php b/src/Attributes/ParameterTrait.php index 30a30a12c..ea5fa34b6 100644 --- a/src/Attributes/ParameterTrait.php +++ b/src/Attributes/ParameterTrait.php @@ -11,6 +11,7 @@ trait ParameterTrait { /** + * @param string|class-string|object|null $ref * @param array $examples * @param array|JsonContent|XmlContent|Attachable|null $content * @param array|null $x diff --git a/src/Attributes/Property.php b/src/Attributes/Property.php index a5cc22ddd..d1c7e24ea 100644 --- a/src/Attributes/Property.php +++ b/src/Attributes/Property.php @@ -12,6 +12,7 @@ class Property extends \OpenApi\Annotations\Property { /** + * @param string|class-string|object|null $ref * @param string[] $required * @param Property[] $properties * @param int|float $maximum diff --git a/src/Attributes/RequestBody.php b/src/Attributes/RequestBody.php index 82e41d717..35ac38f14 100644 --- a/src/Attributes/RequestBody.php +++ b/src/Attributes/RequestBody.php @@ -13,6 +13,7 @@ class RequestBody extends OA\RequestBody { /** + * @param string|class-string|object|null $ref * @param array|JsonContent|XmlContent|Attachable|null $content * @param array|null $x * @param Attachable[]|null $attachables diff --git a/src/Attributes/Response.php b/src/Attributes/Response.php index 996a69720..b79b16d27 100644 --- a/src/Attributes/Response.php +++ b/src/Attributes/Response.php @@ -13,6 +13,7 @@ class Response extends OA\Response { /** + * @param string|class-string|object|null $ref * @param Header[] $headers * @param MediaType|JsonContent|XmlContent|Attachable|array $content * @param Link[] $links diff --git a/src/Attributes/Schema.php b/src/Attributes/Schema.php index 8286df404..b8559491c 100644 --- a/src/Attributes/Schema.php +++ b/src/Attributes/Schema.php @@ -12,6 +12,7 @@ class Schema extends \OpenApi\Annotations\Schema { /** + * @param string|class-string|object|null $ref * @param string[] $required * @param Property[] $properties * @param int|float $maximum diff --git a/src/Attributes/SecurityScheme.php b/src/Attributes/SecurityScheme.php index f1d8bec4a..ea98cab90 100644 --- a/src/Attributes/SecurityScheme.php +++ b/src/Attributes/SecurityScheme.php @@ -12,9 +12,10 @@ class SecurityScheme extends \OpenApi\Annotations\SecurityScheme { /** - * @param Flow[] $flows - * @param array|null $x - * @param Attachable[]|null $attachables + * @param string|class-string|object|null $ref + * @param Flow[] $flows + * @param array|null $x + * @param Attachable[]|null $attachables */ public function __construct( string|object|null $ref = null, diff --git a/src/Attributes/XmlContent.php b/src/Attributes/XmlContent.php index d02fa085a..d5809a7c1 100644 --- a/src/Attributes/XmlContent.php +++ b/src/Attributes/XmlContent.php @@ -12,6 +12,7 @@ class XmlContent extends \OpenApi\Annotations\XmlContent { /** + * @param string|class-string|object|null $ref * @param array $examples * @param string[] $required * @param int|float $maximum diff --git a/src/Processors/AugmentProperties.php b/src/Processors/AugmentProperties.php index e430a93be..2073870e6 100644 --- a/src/Processors/AugmentProperties.php +++ b/src/Processors/AugmentProperties.php @@ -17,6 +17,7 @@ class AugmentProperties implements ProcessorInterface { use Concerns\DocblockTrait; + use Concerns\RefTrait; use Concerns\TypesTrait; public function __invoke(Analysis $analysis) @@ -67,13 +68,6 @@ public function __invoke(Analysis $analysis) } } - protected function toRefKey(Context $context, ?string $name): string - { - $fqn = strtolower($context->fullyQualifiedName($name)); - - return ltrim($fqn, '\\'); - } - protected function augmentType(Analysis $analysis, OA\Property $property, Context $context, array $refs, array $varMatches): void { // docblock typehints diff --git a/src/Processors/AugmentRefs.php b/src/Processors/AugmentRefs.php index 96e056b7b..9328d19a1 100644 --- a/src/Processors/AugmentRefs.php +++ b/src/Processors/AugmentRefs.php @@ -10,12 +10,20 @@ use OpenApi\Annotations as OA; use OpenApi\Generator; -/** - * Update refs broken due to `allOf` augmenting. - */ class AugmentRefs implements ProcessorInterface { + use Concerns\RefTrait; + public function __invoke(Analysis $analysis) + { + $this->resolveAllOfRefs($analysis); + $this->resolveFQCNRefs($analysis); + } + + /** + * Update refs broken due to `allOf` augmenting. + */ + protected function resolveAllOfRefs(Analysis $analysis) { /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class); @@ -46,4 +54,19 @@ public function __invoke(Analysis $analysis) } } } + + protected function resolveFQCNRefs(Analysis $analysis) + { + /** @var OA\AbstractAnnotation[] $annotations */ + $annotations = $analysis->getAnnotationsOfType([OA\Examples::class, OA\Header::class, OA\Link::class, OA\Parameter::class, OA\PathItem::class, OA\RequestBody::class, OA\Response::class, OA\Schema::class, OA\SecurityScheme::class]); + + foreach ($annotations as $annotation) { + if (property_exists($annotation, 'ref') && !Generator::isDefault($annotation->ref) && is_string($annotation->ref) && !$this->isRef($annotation->ref)) { + // check if we have a schema for this + if ($refSchema = $analysis->getSchemaForSource($annotation->ref)) { + $annotation->ref = OA\Components::ref($refSchema); + } + } + } + } } diff --git a/src/Processors/Concerns/RefTrait.php b/src/Processors/Concerns/RefTrait.php new file mode 100644 index 000000000..a97b19589 --- /dev/null +++ b/src/Processors/Concerns/RefTrait.php @@ -0,0 +1,24 @@ +fullyQualifiedName($name)); + + return ltrim($fqn, '\\'); + } + + protected function isRef(?string $ref): bool + { + return $ref && 0 === strpos($ref, '#/'); + } +} diff --git a/src/Processors/ExpandEnums.php b/src/Processors/ExpandEnums.php index 02fe81228..0d4ae2b2b 100644 --- a/src/Processors/ExpandEnums.php +++ b/src/Processors/ExpandEnums.php @@ -82,11 +82,10 @@ protected function expandSchemaEnum(Analysis $analysis): void } else { throw new \InvalidArgumentException("Unexpected enum value, requires specifying the Enum class string: $schema->enum"); } - } elseif (is_array($schema->enum)) { + } else { // might be an array of \UnitEnum::class, string, int, etc... + assert(is_array($schema->enum)); $cases = $schema->enum; - } else { - throw new \InvalidArgumentException('Unexpected enum value, requires Enum class string or array'); } $enums = []; diff --git a/tests/Fixtures/Apis/Attributes/basic.php b/tests/Fixtures/Apis/Attributes/basic.php index 1c25d88cf..a94d43203 100644 --- a/tests/Fixtures/Apis/Attributes/basic.php +++ b/tests/Fixtures/Apis/Attributes/basic.php @@ -110,7 +110,7 @@ class ProductController #[OAT\Response( response: 200, description: 'successful operation', - content: [new OAT\MediaType(mediaType: 'application/json', schema: new OAT\Schema(ref: '#/components/schemas/Product'))], + content: [new OAT\MediaType(mediaType: 'application/json', schema: new OAT\Schema(ref: Product::class))], headers: [ new OAT\Header(header: 'X-Rate-Limit', description: 'calls per hour allowed by the user', schema: new OAT\Schema(type: 'integer', format: 'int32')), ] @@ -126,7 +126,7 @@ public function getProduct( #[OAT\Response( response: 200, description: 'successful operation', - content: new OAT\JsonContent(ref: '#/components/schemas/Product') + content: new OAT\JsonContent(ref: Product::class) )] #[OAT\RequestBody( required: true, @@ -157,7 +157,7 @@ public function addProduct() new OAT\Property( property: 'data', type: 'array', - items: new OAT\Items(ref: '#/components/schemas/Product') + items: new OAT\Items(ref: Product::class) ), ] ) diff --git a/tests/Fixtures/Apis/Mixed/basic.php b/tests/Fixtures/Apis/Mixed/basic.php index a30113817..d398e9bf8 100644 --- a/tests/Fixtures/Apis/Mixed/basic.php +++ b/tests/Fixtures/Apis/Mixed/basic.php @@ -133,7 +133,7 @@ class ProductController new OAT\Response( response: 200, description: 'successful operation', - content: new OAT\JsonContent(ref: '#/components/schemas/Product'), + content: new OAT\JsonContent(ref: Product::class), headers: [ new OAT\Header(header: 'X-Rate-Limit', description: 'calls per hour allowed by the user', schema: new OAT\Schema(type: 'integer', format: 'int32')), ] @@ -187,7 +187,7 @@ public function addProduct() new OAT\Property( property: 'data', type: 'array', - items: new OAT\Items(ref: '#/components/schemas/Product') + items: new OAT\Items(ref: Product::class) ), ] ) diff --git a/tests/Fixtures/Scratch/ClassRef.php b/tests/Fixtures/Scratch/ClassRef.php new file mode 100644 index 000000000..b0b12154a --- /dev/null +++ b/tests/Fixtures/Scratch/ClassRef.php @@ -0,0 +1,29 @@ +