From 862e38b30af71011bc2b8f4c45e30b58b86edab6 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Fri, 2 Aug 2024 13:04:23 +0300 Subject: [PATCH 01/34] improve class analisys logic --- src/Infer/Analyzer/ClassAnalyzer.php | 47 +++++++++++++++---- src/Infer/Definition/ClassDefinition.php | 2 + .../InferExtensions/JsonResourceTypeInfer.php | 5 +- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/Infer/Analyzer/ClassAnalyzer.php b/src/Infer/Analyzer/ClassAnalyzer.php index 7fe94f23..0b6dd0c7 100644 --- a/src/Infer/Analyzer/ClassAnalyzer.php +++ b/src/Infer/Analyzer/ClassAnalyzer.php @@ -29,7 +29,9 @@ private function shouldAnalyzeParentClass(ReflectionClass $parentClassReflection * Classes from `vendor` aren't analyzed at the moment. Instead, it is up to developers to provide * definitions for them using the dictionaries. */ - return ! str_contains($parentClassReflection->getFileName(), DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR); + + return ! str_contains($parentClassReflection->getFileName(), + DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR); } /** @@ -51,7 +53,35 @@ public function analyze(string $name): ClassDefinition // @todo: Here we still want to fire the event, so we can add some details to the definition. $parentDefinition = new ClassDefinition($parentName = $classReflection->getParentClass()->name); - Context::getInstance()->extensionsBroker->afterClassDefinitionCreated(new ClassDefinitionCreatedEvent($parentDefinition->name, $parentDefinition)); + Context::getInstance()->extensionsBroker->afterClassDefinitionCreated(new ClassDefinitionCreatedEvent($parentDefinition->name, + $parentDefinition)); + } + + $annotations = []; + + if ($doc = $classReflection->getDocComment()) { + preg_match_all( + '/@(\w+)(\s\w+)?/', + $doc, + $annotations, + ); + + $annotations = array_map( + static fn($value) => empty($value) ? null : trim($value), + array_combine($annotations[1], $annotations[2]), + ); + } + + $attrs = $classReflection->getAttributes() ?? []; + + foreach ($attrs as $index => $attr) { + if (! class_exists($attr->getName())) { + unset($attrs[$index]); + + continue; + } + + $attrs[$index] = new ($attr->getName())(...$attr->getArguments()); } /* @@ -61,7 +91,7 @@ public function analyze(string $name): ClassDefinition $classDefinition = new ClassDefinition( name: $name, templateTypes: $parentDefinition?->templateTypes ?: [], - properties: array_map(fn ($pd) => clone $pd, $parentDefinition?->properties ?: []), + properties: array_map(fn($pd) => clone $pd, $parentDefinition?->properties ?: []), methods: $parentDefinition?->methods ?: [], parentFqn: $parentName ?? null, ); @@ -79,12 +109,12 @@ public function analyze(string $name): ClassDefinition if ($reflectionProperty->isStatic()) { $classDefinition->properties[$reflectionProperty->name] = new ClassPropertyDefinition( type: $reflectionProperty->hasDefaultValue() - ? (TypeHelper::createTypeFromValue($reflectionProperty->getDefaultValue()) ?: new UnknownType) - : new UnknownType, + ? (TypeHelper::createTypeFromValue($reflectionProperty->getDefaultValue()) ?: new UnknownType()) + : new UnknownType(), ); } else { $classDefinition->properties[$reflectionProperty->name] = new ClassPropertyDefinition( - type: $t = new TemplateType('T'.Str::studly($reflectionProperty->name)), + type: $t = new TemplateType('T' . Str::studly($reflectionProperty->name)), defaultType: $reflectionProperty->hasDefaultValue() ? TypeHelper::createTypeFromValue($reflectionProperty->getDefaultValue()) : null, @@ -102,7 +132,7 @@ public function analyze(string $name): ClassDefinition new FunctionType( $reflectionMethod->name, arguments: [], - returnType: new UnknownType, + returnType: new UnknownType(), ), definingClassName: $name, ); @@ -110,7 +140,8 @@ public function analyze(string $name): ClassDefinition $this->index->registerClassDefinition($classDefinition); - Context::getInstance()->extensionsBroker->afterClassDefinitionCreated(new ClassDefinitionCreatedEvent($classDefinition->name, $classDefinition)); + Context::getInstance()->extensionsBroker->afterClassDefinitionCreated(new ClassDefinitionCreatedEvent($classDefinition->name, + $classDefinition)); return $classDefinition; } diff --git a/src/Infer/Definition/ClassDefinition.php b/src/Infer/Definition/ClassDefinition.php index 27e4cefc..9efd1753 100644 --- a/src/Infer/Definition/ClassDefinition.php +++ b/src/Infer/Definition/ClassDefinition.php @@ -30,6 +30,8 @@ public function __construct( /** @var array $methods */ public array $methods = [], public ?string $parentFqn = null, + public array $annotations = [], + public array $attributes = [], ) {} public function isInstanceOf(string $className) diff --git a/src/Support/InferExtensions/JsonResourceTypeInfer.php b/src/Support/InferExtensions/JsonResourceTypeInfer.php index bf38532e..870aafa6 100644 --- a/src/Support/InferExtensions/JsonResourceTypeInfer.php +++ b/src/Support/InferExtensions/JsonResourceTypeInfer.php @@ -128,7 +128,7 @@ public function getType(Expr $node, Scope $scope): ?Type return null; } - private static function modelType(ClassDefinition $jsonClass, Scope $scope): ?Type + public static function modelType(ClassDefinition $jsonClass, Scope $scope): ?Type { if ([$cachedModelType, $cachedModelDefinition] = static::$jsonResourcesModelTypesCache[$jsonClass->name] ?? null) { if ($cachedModelDefinition) { @@ -170,7 +170,8 @@ private static function getModelName(string $jsonResourceClassName, \ReflectionC ->first(fn ($str) => Str::is(['*@property*$resource', '*@mixin*'], $str)); if ($mixinOrPropertyLine) { - $modelName = Str::replace(['@property', '$resource', '@mixin', ' ', '*'], '', $mixinOrPropertyLine); + $modelName = Str::replace(['@property-read', '$resource', '@mixin', ' ', '*'], '', $mixinOrPropertyLine); + $modelName = Str::replace('@property','', $modelName); $modelClass = $getFqName($modelName); From d9ce4fe005ddec71cf72a77ae1034c7f45755c42 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Fri, 2 Aug 2024 19:07:55 +0300 Subject: [PATCH 02/34] improve class analisys logic --- src/Generator.php | 16 ++++++++-------- src/Infer/Analyzer/ClassAnalyzer.php | 13 ++++++------- src/Support/Generator/OpenApi.php | 14 ++++++++------ .../RulesExtractor/RulesToParameters.php | 2 +- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/Generator.php b/src/Generator.php index bb480c0e..add7d6d8 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -93,7 +93,7 @@ public function __invoke(?GeneratorConfig $config = null) return $openApi->toArray(); } - private function makeOpenApi(GeneratorConfig $config) + protected function makeOpenApi(GeneratorConfig $config) { $openApi = OpenApi::make('3.1.0') ->setComponents($this->transformer->getComponents()) @@ -118,7 +118,7 @@ private function makeOpenApi(GeneratorConfig $config) return $openApi; } - private function getRoutes(GeneratorConfig $config): Collection + protected function getRoutes(GeneratorConfig $config): Collection { return collect(RouteFacade::getRoutes()) ->pipe(function (Collection $c) { @@ -149,7 +149,7 @@ private function getRoutes(GeneratorConfig $config): Collection ->values(); } - private function routeToOperation(OpenApi $openApi, Route $route, GeneratorConfig $config) + protected function routeToOperation(OpenApi $openApi, Route $route, GeneratorConfig $config) { $routeInfo = new RouteInfo($route, $this->fileParser, $this->infer); @@ -164,7 +164,7 @@ private function routeToOperation(OpenApi $openApi, Route $route, GeneratorConfi return $operation; } - private function ensureSchemaTypes(Route $route, Operation $operation): void + protected function ensureSchemaTypes(Route $route, Operation $operation): void { if (! Scramble::getSchemaValidator()->hasRules()) { return; @@ -183,14 +183,14 @@ private function ensureSchemaTypes(Route $route, Operation $operation): void } } - private function createSchemaEnforceTraverser(Route $route) + protected function createSchemaEnforceTraverser(Route $route) { $traverser = new OpenApiTraverser([$visitor = new SchemaEnforceVisitor($route, $this->throwExceptions, $this->exceptions)]); return [$traverser, $visitor]; } - private function moveSameAlternativeServersToPath(OpenApi $openApi) + protected function moveSameAlternativeServersToPath(OpenApi $openApi) { foreach (collect($openApi->paths)->groupBy('path') as $pathsGroup) { if ($pathsGroup->isEmpty()) { @@ -217,7 +217,7 @@ private function moveSameAlternativeServersToPath(OpenApi $openApi) } } - private function setUniqueOperationId(OpenApi $openApi) + protected function setUniqueOperationId(OpenApi $openApi) { $names = new UniqueNamesOptionsCollection; @@ -234,7 +234,7 @@ private function setUniqueOperationId(OpenApi $openApi) }); } - private function foreachOperation(OpenApi $openApi, callable $callback) + protected function foreachOperation(OpenApi $openApi, callable $callback) { foreach (collect($openApi->paths)->groupBy('path') as $pathsGroup) { if ($pathsGroup->isEmpty()) { diff --git a/src/Infer/Analyzer/ClassAnalyzer.php b/src/Infer/Analyzer/ClassAnalyzer.php index 0b6dd0c7..d1165f4b 100644 --- a/src/Infer/Analyzer/ClassAnalyzer.php +++ b/src/Infer/Analyzer/ClassAnalyzer.php @@ -94,6 +94,8 @@ public function analyze(string $name): ClassDefinition properties: array_map(fn($pd) => clone $pd, $parentDefinition?->properties ?: []), methods: $parentDefinition?->methods ?: [], parentFqn: $parentName ?? null, + annotations: $annotations, + attributes: $attrs, ); /* @@ -124,13 +126,9 @@ public function analyze(string $name): ClassDefinition } foreach ($classReflection->getMethods() as $reflectionMethod) { - if ($reflectionMethod->class !== $name) { - continue; - } - $classDefinition->methods[$reflectionMethod->name] = new FunctionLikeDefinition( new FunctionType( - $reflectionMethod->name, + name: $reflectionMethod->name, arguments: [], returnType: new UnknownType(), ), @@ -140,8 +138,9 @@ public function analyze(string $name): ClassDefinition $this->index->registerClassDefinition($classDefinition); - Context::getInstance()->extensionsBroker->afterClassDefinitionCreated(new ClassDefinitionCreatedEvent($classDefinition->name, - $classDefinition)); + Context::getInstance()->extensionsBroker->afterClassDefinitionCreated( + new ClassDefinitionCreatedEvent($classDefinition->name, $classDefinition), + ); return $classDefinition; } diff --git a/src/Support/Generator/OpenApi.php b/src/Support/Generator/OpenApi.php index 5a7bf8e2..a0cbae70 100644 --- a/src/Support/Generator/OpenApi.php +++ b/src/Support/Generator/OpenApi.php @@ -2,6 +2,8 @@ namespace Dedoc\Scramble\Support\Generator; +use stdClass; + class OpenApi { public string $version; @@ -21,7 +23,7 @@ class OpenApi public function __construct(string $version) { $this->version = $version; - $this->components = new Components; + $this->components = new Components(); } public static function make(string $version) @@ -56,7 +58,7 @@ public function setInfo(InfoObject $info) } /** - * @param Path[] $paths + * @param Path[] $paths */ public function paths(array $paths) { @@ -90,12 +92,12 @@ public function toArray() { $result = [ 'openapi' => $this->version, - 'info' => $this->info->toArray(), + 'info' => isset($this->info) ? $this->info->toArray() : new StdClass(), ]; if (count($this->servers)) { $result['servers'] = array_map( - fn (Server $s) => $s->toArray(), + fn(Server $s) => $s->toArray(), $this->servers, ); } @@ -108,8 +110,8 @@ public function toArray() $paths = []; foreach ($this->paths as $pathBuilder) { - $paths['/'.$pathBuilder->path] = array_merge( - $paths['/'.$pathBuilder->path] ?? [], + $paths['/' . $pathBuilder->path] = array_merge( + $paths['/' . $pathBuilder->path] ?? [], $pathBuilder->toArray(), ); } diff --git a/src/Support/OperationExtensions/RulesExtractor/RulesToParameters.php b/src/Support/OperationExtensions/RulesExtractor/RulesToParameters.php index 99d2fadb..a20f9d35 100644 --- a/src/Support/OperationExtensions/RulesExtractor/RulesToParameters.php +++ b/src/Support/OperationExtensions/RulesExtractor/RulesToParameters.php @@ -36,7 +36,7 @@ public function __construct(array $rules, array $validationNodesResults, TypeTra $this->nodeDocs = $this->extractNodeDocs(); } - public function handle() + public function handle(): array { return collect($this->rules) ->pipe($this->handleConfirmed(...)) From 504cf825cff0fa28b42572a135029ec487fa67e6 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Wed, 7 Aug 2024 19:12:38 +0300 Subject: [PATCH 03/34] add ability for model to resolve casts --- src/Generator.php | 56 +++++----- src/Support/Generator/TypeTransformer.php | 97 +++++++++-------- src/Support/ResponseExtractor/ModelInfo.php | 109 ++++++++++++-------- src/Support/Type/TemplateType.php | 2 +- 4 files changed, 153 insertions(+), 111 deletions(-) diff --git a/src/Generator.php b/src/Generator.php index add7d6d8..caff2004 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -29,11 +29,11 @@ class Generator protected bool $throwExceptions = true; public function __construct( - private TypeTransformer $transformer, - private OperationBuilder $operationBuilder, - private ServerFactory $serverFactory, - private FileParser $fileParser, - private Infer $infer + protected TypeTransformer $transformer, + protected OperationBuilder $operationBuilder, + protected ServerFactory $serverFactory, + protected FileParser $fileParser, + protected Infer $infer, ) {} public function setThrowExceptions(bool $throwExceptions): static @@ -44,6 +44,11 @@ public function setThrowExceptions(bool $throwExceptions): static } public function __invoke(?GeneratorConfig $config = null) + { + return $this->generate($config)->toArray(); + } + + public function generate(?GeneratorConfig $config = null): OpenApi { $config ??= (new GeneratorConfig(config('scramble'))) ->routes(Scramble::$routeResolver) @@ -64,21 +69,21 @@ public function __invoke(?GeneratorConfig $config = null) $method = $route->methods()[0]; $action = $route->getAction('uses'); - dump("Error when analyzing route '$method $route->uri' ($action): {$e->getMessage()} – ".($e->getFile().' on line '.$e->getLine())); - logger()->error("Error when analyzing route '$method $route->uri' ($action): {$e->getMessage()} – ".($e->getFile().' on line '.$e->getLine())); + dump("Error when analyzing route '$method $route->uri' ($action): {$e->getMessage()} – " . ($e->getFile() . ' on line ' . $e->getLine())); + logger()->error("Error when analyzing route '$method $route->uri' ($action): {$e->getMessage()} – " . ($e->getFile() . ' on line ' . $e->getLine())); } throw $e; } }) ->filter() // Closure based routes are filtered out for now, right here - ->sortBy(fn (Operation $o) => $o->tags[0] ?? $o->description) - ->each(fn (Operation $operation) => $openApi->addPath( + ->sortBy(fn(Operation $o) => $o->tags[0] ?? $o->description) + ->each(fn(Operation $operation) => $openApi->addPath( Path::make( (string) Str::of($operation->path) ->replaceFirst($config->get('api_path', 'api'), '') - ->trim('/') - )->addOperation($operation) + ->trim('/'), + )->addOperation($operation), )) ->toArray(); @@ -90,7 +95,7 @@ public function __invoke(?GeneratorConfig $config = null) $afterOpenApiGenerated($openApi); } - return $openApi->toArray(); + return $openApi; } protected function makeOpenApi(GeneratorConfig $config) @@ -100,18 +105,19 @@ protected function makeOpenApi(GeneratorConfig $config) ->setInfo( InfoObject::make($config->get('ui.title', $default = config('app.name')) ?: $default) ->setVersion($config->get('info.version', '0.0.1')) - ->setDescription($config->get('info.description', '')) + ->setDescription($config->get('info.description', '')), ); [$defaultProtocol] = explode('://', url('/')); - $servers = $config->get('servers') ?: [ - '' => ($domain = $config->get('api_domain')) - ? $defaultProtocol.'://'.$domain.'/'.$config->get('api_path', 'api') - : $config->get('api_path', 'api'), - ]; + $servers = $config->get('servers') + ?: [ + '' => ($domain = $config->get('api_domain')) + ? $defaultProtocol . '://' . $domain . '/' . $config->get('api_path', 'api') + : $config->get('api_path', 'api'), + ]; foreach ($servers as $description => $url) { $openApi->addServer( - $this->serverFactory->make(url($url ?: '/'), $description) + $this->serverFactory->make(url($url ?: '/'), $description), ); } @@ -145,7 +151,7 @@ protected function getRoutes(GeneratorConfig $config): Collection return ! ($name = $route->getAction('as')) || ! Str::startsWith($name, 'scramble'); }) ->filter($config->routes()) - ->filter(fn (Route $r) => $r->getAction('controller')) + ->filter(fn(Route $r) => $r->getAction('controller')) ->values(); } @@ -185,7 +191,9 @@ protected function ensureSchemaTypes(Route $route, Operation $operation): void protected function createSchemaEnforceTraverser(Route $route) { - $traverser = new OpenApiTraverser([$visitor = new SchemaEnforceVisitor($route, $this->throwExceptions, $this->exceptions)]); + $traverser = new OpenApiTraverser([$visitor = new SchemaEnforceVisitor($route, + $this->throwExceptions, + $this->exceptions)]); return [$traverser, $visitor]; } @@ -200,9 +208,9 @@ protected function moveSameAlternativeServersToPath(OpenApi $openApi) $operations = collect($pathsGroup->pluck('operations')->flatten()); $operationsHaveSameAlternativeServers = $operations->count() - && $operations->every(fn (Operation $o) => count($o->servers)) + && $operations->every(fn(Operation $o) => count($o->servers)) && $operations->unique(function (Operation $o) { - return collect($o->servers)->map(fn (Server $s) => $s->url)->join('.'); + return collect($o->servers)->map(fn(Server $s) => $s->url)->join('.'); })->count() === 1; if (! $operationsHaveSameAlternativeServers) { @@ -219,7 +227,7 @@ protected function moveSameAlternativeServersToPath(OpenApi $openApi) protected function setUniqueOperationId(OpenApi $openApi) { - $names = new UniqueNamesOptionsCollection; + $names = new UniqueNamesOptionsCollection(); $this->foreachOperation($openApi, function (Operation $operation) use ($names) { $names->push($operation->getAttribute('operationId')); diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index b46d5e8a..f80636c9 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -42,7 +42,7 @@ public function __construct( Infer $infer, Components $components, array $typeToSchemaExtensions = [], - array $exceptionToResponseExtensions = [] + array $exceptionToResponseExtensions = [], ) { $this->infer = $infer; $this->components = $components; @@ -50,6 +50,13 @@ public function __construct( $this->exceptionToResponseExtensions = $exceptionToResponseExtensions; } + public function resetState(): static + { + $this->components = new Components(); + + return $this; + } + public function getComponents(): Components { return $this->components; @@ -57,7 +64,7 @@ public function getComponents(): Components public function transform(Type $type) { - $openApiType = new UnknownType; + $openApiType = new UnknownType(); if ($type instanceof TemplateType && $type->is) { $type = $type->is; @@ -68,21 +75,21 @@ public function transform(Type $type) && $type->isList ) { /** @see https://stackoverflow.com/questions/57464633/how-to-define-a-json-array-with-concrete-item-definition-for-every-index-i-e-a */ - $openApiType = (new ArrayType) + $openApiType = (new ArrayType()) ->setMin(count($type->items)) ->setMax(count($type->items)) ->setPrefixItems( array_map( - fn ($item) => $this->transform($item->value), - $type->items - ) + fn($item) => $this->transform($item->value), + $type->items, + ), ) ->setAdditionalItems(false); } elseif ( $type instanceof \Dedoc\Scramble\Support\Type\KeyedArrayType && ! $type->isList ) { - $openApiType = new ObjectType; + $openApiType = new ObjectType(); $requiredKeys = []; $props = collect($type->items) @@ -105,9 +112,9 @@ public function transform(Type $type) $keyType = $this->transform($type->key); if ($keyType instanceof IntegerType) { - $openApiType = (new ArrayType)->setItems($this->transform($type->value)); + $openApiType = (new ArrayType())->setItems($this->transform($type->value)); } else { - $openApiType = (new ObjectType) + $openApiType = (new ObjectType()) ->additionalProperties($this->transform($type->value)); } } elseif ($type instanceof ArrayItemType_) { @@ -121,13 +128,15 @@ public function transform(Type $type) ? $this->transform(PhpDocTypeHelper::toType($varNode->type)) : $openApiType; - $commentDescription = trim($docNode->getAttribute('summary').' '.$docNode->getAttribute('description')); + $commentDescription = trim($docNode->getAttribute('summary') . ' ' . $docNode->getAttribute('description')); $varNodeDescription = $varNode && $varNode->description ? trim($varNode->description) : ''; if ($commentDescription || $varNodeDescription) { - $openApiType->setDescription(implode('. ', array_filter([$varNodeDescription, $commentDescription]))); + $openApiType->setDescription(implode('. ', + array_filter([$varNodeDescription, $commentDescription]))); } - if ($examples = ExamplesExtractor::make($docNode)->extract(preferString: $openApiType instanceof StringType)) { + if ($examples = ExamplesExtractor::make($docNode) + ->extract(preferString: $openApiType instanceof StringType)) { $openApiType->examples($examples); } @@ -136,59 +145,59 @@ public function transform(Type $type) } } } elseif ($type instanceof Union) { - if (count($type->types) === 2 && collect($type->types)->contains(fn ($t) => $t instanceof \Dedoc\Scramble\Support\Type\NullType)) { - $notNullType = collect($type->types)->first(fn ($t) => ! ($t instanceof \Dedoc\Scramble\Support\Type\NullType)); + if (count($type->types) === 2 && collect($type->types)->contains(fn($t) => $t instanceof \Dedoc\Scramble\Support\Type\NullType)) { + $notNullType = collect($type->types)->first(fn($t) => ! ($t instanceof \Dedoc\Scramble\Support\Type\NullType)); if ($notNullType) { $openApiType = $this->transform($notNullType)->nullable(true); } else { - $openApiType = new NullType; + $openApiType = new NullType(); } } else { [$literals, $otherTypes] = collect($type->types) - ->partition(fn ($t) => $t instanceof LiteralStringType || $t instanceof LiteralIntegerType); + ->partition(fn($t) => $t instanceof LiteralStringType || $t instanceof LiteralIntegerType); [$stringLiterals, $integerLiterals] = collect($literals) - ->partition(fn ($t) => $t instanceof LiteralStringType); + ->partition(fn($t) => $t instanceof LiteralStringType); $items = array_map($this->transform(...), $otherTypes->values()->toArray()); if ($stringLiterals->count()) { - $items[] = (new StringType)->enum( - $stringLiterals->map->value->unique()->toArray() + $items[] = (new StringType())->enum( + $stringLiterals->map->value->unique()->toArray(), ); } if ($integerLiterals->count()) { - $items[] = (new IntegerType)->enum( - $integerLiterals->map->value->unique()->toArray() + $items[] = (new IntegerType())->enum( + $integerLiterals->map->value->unique()->toArray(), ); } // Removing duplicated schemas before making a resulting AnyOf type. - $uniqueItems = collect($items)->unique(fn ($i) => json_encode($i->toArray()))->values()->all(); - $openApiType = count($uniqueItems) === 1 ? $uniqueItems[0] : (new AnyOf)->setItems($uniqueItems); + $uniqueItems = collect($items)->unique(fn($i) => json_encode($i->toArray()))->values()->all(); + $openApiType = count($uniqueItems) === 1 ? $uniqueItems[0] : (new AnyOf())->setItems($uniqueItems); } } elseif ($type instanceof LiteralStringType) { - $openApiType = (new StringType)->example($type->value); + $openApiType = (new StringType())->example($type->value); } elseif ($type instanceof LiteralIntegerType) { - $openApiType = (new IntegerType)->example($type->value); + $openApiType = (new IntegerType())->example($type->value); } elseif ($type instanceof LiteralFloatType) { - $openApiType = (new NumberType)->example($type->value); + $openApiType = (new NumberType())->example($type->value); } elseif ($type instanceof \Dedoc\Scramble\Support\Type\StringType) { - $openApiType = new StringType; + $openApiType = new StringType(); } elseif ($type instanceof \Dedoc\Scramble\Support\Type\FloatType) { - $openApiType = new NumberType; + $openApiType = new NumberType(); } elseif ($type instanceof \Dedoc\Scramble\Support\Type\IntegerType) { - $openApiType = new IntegerType; + $openApiType = new IntegerType(); } elseif ($type instanceof \Dedoc\Scramble\Support\Type\BooleanType) { - $openApiType = new BooleanType; + $openApiType = new BooleanType(); } elseif ($type instanceof \Dedoc\Scramble\Support\Type\NullType) { - $openApiType = new NullType; + $openApiType = new NullType(); } elseif ($type instanceof \Dedoc\Scramble\Support\Type\ObjectType) { - $openApiType = new ObjectType; + $openApiType = new ObjectType(); } elseif ($type instanceof \Dedoc\Scramble\Support\Type\IntersectionType) { - $openApiType = (new AllOf)->setItems(array_filter(array_map( - fn ($t) => $this->transform($t), + $openApiType = (new AllOf())->setItems(array_filter(array_map( + fn($t) => $this->transform($t), $type->types, ))); } @@ -233,7 +242,8 @@ function ($acc, $extensionClass) use ($type) { } if ($reference) { - $this->components->addSchema($reference->fullName, Schema::fromType(new UnknownType('Reference is being analyzed.'))); + $this->components->addSchema($reference->fullName, + Schema::fromType(new UnknownType('Reference is being analyzed.'))); } if ($handledType = $extension->toSchema($type, $acc)) { @@ -252,7 +262,7 @@ function ($acc, $extensionClass) use ($type) { } return $acc; - } + }, ); } @@ -261,7 +271,10 @@ public function toResponse(Type $type) // In case of union type being returned and all of its types resulting in the same response, we want to make // sure to take only unique types to avoid having the same types in the response. if ($type instanceof Union) { - $uniqueItems = collect($type->types)->unique(fn ($i) => json_encode($this->transform($i)->toArray()))->values()->all(); + $uniqueItems = collect($type->types) + ->unique(fn($i) => json_encode($this->transform($i)->toArray())) + ->values() + ->all(); $type = count($uniqueItems) === 1 ? $uniqueItems[0] : Union::wrap($uniqueItems); } @@ -273,15 +286,15 @@ public function toResponse(Type $type) $response = Response::make(200) ->setContent( 'application/json', - Schema::fromType($this->transform($type)) + Schema::fromType($this->transform($type)), ); } /** @var PhpDocNode $docNode */ if ($docNode = $type->getAttribute('docNode')) { $description = (string) Str::of($docNode->getAttribute('summary') ?: '') - ->append("\n\n".($docNode->getAttribute('description') ?: '')) - ->append("\n\n".$response->description) + ->append("\n\n" . ($docNode->getAttribute('description') ?: '')) + ->append("\n\n" . $response->description) ->trim(); $response->description($description); @@ -317,7 +330,7 @@ function ($acc, $extensionClass) use ($type) { } return $acc; - } + }, ); } @@ -348,7 +361,7 @@ function ($acc, $extensionClass) use ($type) { } return $acc; - } + }, ); } } diff --git a/src/Support/ResponseExtractor/ModelInfo.php b/src/Support/ResponseExtractor/ModelInfo.php index e34fa0ef..8d548d6f 100644 --- a/src/Support/ResponseExtractor/ModelInfo.php +++ b/src/Support/ResponseExtractor/ModelInfo.php @@ -15,6 +15,7 @@ use Dedoc\Scramble\Support\Type\StringType; use Dedoc\Scramble\Support\Type\Union; use Dedoc\Scramble\Support\Type\UnknownType; +use Illuminate\Contracts\Database\Eloquent\CastsAttributes; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Collection; @@ -46,7 +47,7 @@ class ModelInfo ]; public function __construct( - private string $class + private string $class, ) {} public function handle() @@ -92,8 +93,8 @@ public function type() $properties = $modelInfo->get('attributes') ->map(function ($value, $key) use ($model) { $isNullable = $value['nullable']; - $createType = fn ($t) => $isNullable - ? Union::wrap([new NullType, $t]) + $createType = fn($t) => $isNullable + ? Union::wrap([new NullType(), $t]) : $t; $type = explode(' ', $value['type'] ?? ''); @@ -104,21 +105,21 @@ public function type() } $types = [ - 'int' => new IntegerType, - 'integer' => new IntegerType, - 'bigint' => new IntegerType, - 'float' => new FloatType, - 'double' => new FloatType, - 'decimal' => new FloatType, - 'string' => new StringType, - 'varchar' => new StringType, - 'text' => new StringType, - 'datetime' => new StringType, - 'tinyint' => new BooleanType, - 'bool' => new BooleanType, - 'boolean' => new BooleanType, - 'json' => new ArrayType, - 'array' => new ArrayType, + 'int' => new IntegerType(), + 'integer' => new IntegerType(), + 'bigint' => new IntegerType(), + 'float' => new FloatType(), + 'double' => new FloatType(), + 'decimal' => new FloatType(), + 'string' => new StringType(), + 'varchar' => new StringType(), + 'text' => new StringType(), + 'datetime' => new StringType(), + 'tinyint' => new BooleanType(), + 'bool' => new BooleanType(), + 'boolean' => new BooleanType(), + 'json' => new ArrayType(), + 'array' => new ArrayType(), ]; $attributeType = null; @@ -127,15 +128,29 @@ public function type() $attributeType = $createType($types[$typeName]); } - if ($attributeType && $value['cast'] && function_exists('enum_exists') && enum_exists($value['cast'])) { + if ( + $attributeType + && $value['cast'] + && function_exists('enum_exists') + && enum_exists($value['cast']) + ) { if (! isset($value['cast']::cases()[0]->value)) { return $attributeType; } - $attributeType = new ObjectType($value['cast']); + return new ObjectType($value['cast']); } - return $attributeType ?: new UnknownType("unimplemented DB column type [$type[0]]"); + if ( + $attributeType + && $value['cast'] + && class_exists($value['cast']) + && is_subclass_of($value['cast'], CastsAttributes::class) + ) { + return new ObjectType($value['cast']); + } + + return new UnknownType("unimplemented DB column type [$type[0]]"); }); $relations = $modelInfo->get('relations') @@ -145,7 +160,7 @@ public function type() \Illuminate\Database\Eloquent\Collection::class, [ new ObjectType($relation['related']), - ] + ], ); } @@ -154,14 +169,15 @@ public function type() return static::$cache[$this->class] = new ClassDefinition( name: $modelInfo->get('class'), - properties: $properties->merge($relations)->map(fn ($t) => new ClassPropertyDefinition($t))->all(), + properties: $properties->merge($relations)->map(fn($t) => new ClassPropertyDefinition($t))->all(), ); } /** * Get the column attributes for the given model. * - * @param \Illuminate\Database\Eloquent\Model $model + * @param \Illuminate\Database\Eloquent\Model $model + * * @return \Illuminate\Support\Collection */ protected function getAttributes($model) @@ -174,7 +190,7 @@ protected function getAttributes($model) return collect($columns) ->values() - ->map(fn ($column) => [ + ->map(fn($column) => [ 'driver' => $connection->getDriverName(), 'name' => $column['name'], 'type' => $column['type'], @@ -205,15 +221,16 @@ private function getColumnDefault($column, Model $model) private function columnIsUnique($column, array $indexes) { return collect($indexes)->contains( - fn ($index) => count($index['columns']) === 1 && $index['columns'][0] === $column && $index['unique'] + fn($index) => count($index['columns']) === 1 && $index['columns'][0] === $column && $index['unique'], ); } /** * Get the virtual (non-column) attributes for the given model. * - * @param \Illuminate\Database\Eloquent\Model $model - * @param array[] $columns + * @param \Illuminate\Database\Eloquent\Model $model + * @param array[] $columns + * * @return \Illuminate\Support\Collection */ protected function getVirtualAttributes($model, $columns) @@ -224,9 +241,9 @@ protected function getVirtualAttributes($model, $columns) return collect($class->getMethods()) ->reject( - fn (ReflectionMethod $method) => $method->isStatic() + fn(ReflectionMethod $method) => $method->isStatic() || $method->isAbstract() - || $method->getDeclaringClass()->getName() !== get_class($model) + || $method->getDeclaringClass()->getName() !== get_class($model), ) ->mapWithKeys(function (ReflectionMethod $method) use ($model) { if (preg_match('/^get(.*)Attribute$/', $method->getName(), $matches) === 1) { @@ -237,8 +254,8 @@ protected function getVirtualAttributes($model, $columns) return []; } }) - ->reject(fn ($cast, $name) => $keyedColumns->has($name)) - ->map(fn ($cast, $name) => [ + ->reject(fn($cast, $name) => $keyedColumns->has($name)) + ->map(fn($cast, $name) => [ 'driver' => null, 'name' => $name, 'type' => null, @@ -257,15 +274,16 @@ protected function getVirtualAttributes($model, $columns) /** * Get the relations from the given model. * - * @param \Illuminate\Database\Eloquent\Model $model + * @param \Illuminate\Database\Eloquent\Model $model + * * @return \Illuminate\Support\Collection */ protected function getRelations($model) { return collect(get_class_methods($model)) - ->map(fn ($method) => new ReflectionMethod($model, $method)) + ->map(fn($method) => new ReflectionMethod($model, $method)) ->reject( - fn (ReflectionMethod $method) => $method->isStatic() + fn(ReflectionMethod $method) => $method->isStatic() || $method->isAbstract() || $method->getDeclaringClass()->getName() === Model::class, ) @@ -279,7 +297,7 @@ protected function getRelations($model) } return collect($this->relationMethods) - ->contains(fn ($relationMethod) => str_contains($code, '$this->'.$relationMethod.'(')); + ->contains(fn($relationMethod) => str_contains($code, '$this->' . $relationMethod . '(')); }) ->map(function (ReflectionMethod $method) use ($model) { try { @@ -320,8 +338,9 @@ protected function displayJson($model, $class, $attributes, $relations) /** * Get the cast type for the given column. * - * @param string $column - * @param \Illuminate\Database\Eloquent\Model $model + * @param string $column + * @param \Illuminate\Database\Eloquent\Model $model + * * @return string|null */ protected function getCastType($column, $model) @@ -340,7 +359,8 @@ protected function getCastType($column, $model) /** * Get the model casts, including any date casts. * - * @param \Illuminate\Database\Eloquent\Model $model + * @param \Illuminate\Database\Eloquent\Model $model + * * @return \Illuminate\Support\Collection */ protected function getCastsWithDates($model) @@ -348,15 +368,16 @@ protected function getCastsWithDates($model) return collect($model->getDates()) ->filter() ->flip() - ->map(fn () => 'datetime') + ->map(fn() => 'datetime') ->merge($model->getCasts()); } /** * Determine if the given attribute is hidden. * - * @param string $attribute - * @param \Illuminate\Database\Eloquent\Model $model + * @param string $attribute + * @param \Illuminate\Database\Eloquent\Model $model + * * @return bool */ protected function attributeIsHidden($attribute, $model) @@ -400,7 +421,7 @@ protected function qualifyModel(string $model) } return is_dir(app_path('Models')) - ? $rootNamespace.'Models\\'.$model - : $rootNamespace.$model; + ? $rootNamespace . 'Models\\' . $model + : $rootNamespace . $model; } } diff --git a/src/Support/Type/TemplateType.php b/src/Support/Type/TemplateType.php index df027936..11bb3b84 100644 --- a/src/Support/Type/TemplateType.php +++ b/src/Support/Type/TemplateType.php @@ -20,7 +20,7 @@ public function isSame(Type $type) public function isInstanceOf(string $className) { - return $this->is?->isInstanceOf($className); + return (bool) $this->is?->isInstanceOf($className); } public function toString(): string From a122395b88b035166b62ce3f70349b15c15448f4 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Thu, 8 Aug 2024 17:06:16 +0300 Subject: [PATCH 04/34] improve reference, add static make method --- src/Support/Generator/Reference.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Support/Generator/Reference.php b/src/Support/Generator/Reference.php index 77a7ee77..510cf975 100644 --- a/src/Support/Generator/Reference.php +++ b/src/Support/Generator/Reference.php @@ -21,6 +21,15 @@ public function __construct(string $referenceType, string $fullName, Components $this->components = $components; } + public static function make(string $referenceType, string $fullName, Components $components): static + { + return new static( + referenceType: $referenceType, + fullName: $fullName, + components: $components, + ); + } + public function resolve() { return $this->components->get($this); From 3f7a34b83dcb154b5e572a31fe7c8349f3510b20 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Fri, 30 Aug 2024 15:02:34 +0300 Subject: [PATCH 05/34] improve: improve validation rules extraction from the 'rules()' method in controller --- .../OperationExtensions/RequestBodyExtension.php | 2 +- .../RulesExtractor/ValidateCallExtractor.php | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Support/OperationExtensions/RequestBodyExtension.php b/src/Support/OperationExtensions/RequestBodyExtension.php index 2d908984..d3758e57 100644 --- a/src/Support/OperationExtensions/RequestBodyExtension.php +++ b/src/Support/OperationExtensions/RequestBodyExtension.php @@ -167,7 +167,7 @@ protected function extractRouteRequestValidationRules(Route $route, $methodNode) } } - if (($validateCallExtractor = new ValidateCallExtractor($methodNode))->shouldHandle()) { + if (($validateCallExtractor = new ValidateCallExtractor($methodNode, $route))->shouldHandle()) { if ($validateCallRules = $validateCallExtractor->extract()) { $rules = array_merge($rules, $validateCallRules); $nodesResults[] = $validateCallExtractor->node(); diff --git a/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php b/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php index 955f12c7..6a4b907b 100644 --- a/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php +++ b/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php @@ -4,6 +4,7 @@ use Dedoc\Scramble\Support\SchemaClassDocReflector; use Illuminate\Http\Request; +use Illuminate\Routing\Route; use PhpParser\Node; use PhpParser\NodeFinder; use PhpParser\PrettyPrinter\Standard; @@ -11,14 +12,16 @@ class ValidateCallExtractor { - private ?Node\FunctionLike $handle; + protected ?Node\FunctionLike $handle; + protected ?Route $route; - public function __construct(?Node\FunctionLike $handle) + public function __construct(?Node\FunctionLike $handle, ?Route $route = null) { $this->handle = $handle; + $this->route = $route; } - public function shouldHandle() + public function shouldHandle(): bool { return (bool) $this->handle; } @@ -116,7 +119,12 @@ public function extract() try { extract($injectableParams); - $rules = eval("\$request = request(); return $validationRulesCode;"); + if ($this->route) { + $rules = (fn() => eval("\$request = request(); return $validationRulesCode;")) + ->call($this->route->getController()); + } else { + $rules = eval("\$request = request(); return $validationRulesCode;"); + } } catch (\Throwable $exception) { throw $exception; } From 3381d5b944fc0eddb1a1af3e05c3493bd1d0dd92 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Mon, 2 Sep 2024 13:49:19 +0300 Subject: [PATCH 06/34] improve: check the method parameter is actually part of the route while forming the request path --- .../OperationExtensions/RequestEssentialsExtension.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Support/OperationExtensions/RequestEssentialsExtension.php b/src/Support/OperationExtensions/RequestEssentialsExtension.php index 3a604d3c..36884587 100644 --- a/src/Support/OperationExtensions/RequestEssentialsExtension.php +++ b/src/Support/OperationExtensions/RequestEssentialsExtension.php @@ -151,7 +151,12 @@ private function getRoutePathParameters(RouteInfo $routeInfo) $paramNames = $route->parameterNames(); $paramsWithRealNames = ($reflectionParams = collect($route->signatureParameters()) - ->filter(function (ReflectionParameter $v) { + ->filter(function (ReflectionParameter $v) use ($paramNames) { + //check the method parameter is actually part of the route + if (! in_array($v->name, $paramNames)) { + return false; + } + if (($type = $v->getType()) && ($type instanceof \ReflectionNamedType) && ($typeName = $type->getName())) { if (is_a($typeName, Request::class, true)) { return false; @@ -291,7 +296,7 @@ private function getModelIdTypeAndDescription( ? Str::upper($routeKeyName) : (string) Str::of($routeKeyName)->lower()->kebab()->replace(['-', '_'], ' '); - $description = 'The '.Str::of($paramName)->kebab()->replace(['-', '_'], ' ').' '.$keyDescriptionName; + $description = 'The ' . Str::of($paramName)->kebab()->replace(['-', '_'], ' ') . ' ' . $keyDescriptionName; } $modelTraits = class_uses($type->name); From 5552e708b072ca6735df2f60bbd69e5748ee8586 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Wed, 4 Sep 2024 16:55:26 +0300 Subject: [PATCH 07/34] improve: replace api/ correctly in the rendered doc paths --- src/Generator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Generator.php b/src/Generator.php index caff2004..365f33c3 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -81,7 +81,7 @@ public function generate(?GeneratorConfig $config = null): OpenApi ->each(fn(Operation $operation) => $openApi->addPath( Path::make( (string) Str::of($operation->path) - ->replaceFirst($config->get('api_path', 'api'), '') + ->replaceFirst(rtrim($config->get('api_path', 'api'), '/') . '/', '/') ->trim('/'), )->addOperation($operation), )) From bee6b74d409787d5aa21d25952ae78490e4cfa5a Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Thu, 5 Sep 2024 18:37:45 +0300 Subject: [PATCH 08/34] impove: unhardcode reference and type transformer --- .../AuthenticationExceptionToResponseExtension.php | 6 +++++- .../AuthorizationExceptionToResponseExtension.php | 6 +++++- .../NotFoundExceptionToResponseExtension.php | 6 +++++- .../ValidationExceptionToResponseExtension.php | 6 +++++- src/Support/Generator/Components.php | 12 ++++++++++-- src/Support/Generator/TypeTransformer.php | 12 ++++++------ .../OperationExtensions/RequestBodyExtension.php | 6 +++++- src/Support/TypeToSchemaExtensions/EnumToSchema.php | 6 +++++- .../JsonResourceTypeToSchema.php | 6 +++++- src/Support/TypeToSchemaExtensions/ModelToSchema.php | 6 +++++- 10 files changed, 56 insertions(+), 16 deletions(-) diff --git a/src/Support/ExceptionToResponseExtensions/AuthenticationExceptionToResponseExtension.php b/src/Support/ExceptionToResponseExtensions/AuthenticationExceptionToResponseExtension.php index c5412729..3176260a 100644 --- a/src/Support/ExceptionToResponseExtensions/AuthenticationExceptionToResponseExtension.php +++ b/src/Support/ExceptionToResponseExtensions/AuthenticationExceptionToResponseExtension.php @@ -40,6 +40,10 @@ public function toResponse(Type $type) public function reference(ObjectType $type) { - return new Reference('responses', Str::start($type->name, '\\'), $this->components); + return app(Reference::class, [ + 'referenceType' => 'responses', + 'fullName' => Str::start($type->name, '\\'), + 'components' => $this->components, + ]); } } diff --git a/src/Support/ExceptionToResponseExtensions/AuthorizationExceptionToResponseExtension.php b/src/Support/ExceptionToResponseExtensions/AuthorizationExceptionToResponseExtension.php index c4d14d93..eaa37f8e 100644 --- a/src/Support/ExceptionToResponseExtensions/AuthorizationExceptionToResponseExtension.php +++ b/src/Support/ExceptionToResponseExtensions/AuthorizationExceptionToResponseExtension.php @@ -40,6 +40,10 @@ public function toResponse(Type $type) public function reference(ObjectType $type) { - return new Reference('responses', Str::start($type->name, '\\'), $this->components); + return app(Reference::class, [ + 'referenceType' => 'responses', + 'fullName' => Str::start($type->name, '\\'), + 'components' => $this->components, + ]); } } diff --git a/src/Support/ExceptionToResponseExtensions/NotFoundExceptionToResponseExtension.php b/src/Support/ExceptionToResponseExtensions/NotFoundExceptionToResponseExtension.php index a01a910c..0d8bc6ce 100644 --- a/src/Support/ExceptionToResponseExtensions/NotFoundExceptionToResponseExtension.php +++ b/src/Support/ExceptionToResponseExtensions/NotFoundExceptionToResponseExtension.php @@ -44,6 +44,10 @@ public function toResponse(Type $type) public function reference(ObjectType $type) { - return new Reference('responses', Str::start($type->name, '\\'), $this->components); + return app(Reference::class, [ + 'referenceType' => 'responses', + 'fullName' => Str::start($type->name, '\\'), + 'components' => $this->components, + ]); } } diff --git a/src/Support/ExceptionToResponseExtensions/ValidationExceptionToResponseExtension.php b/src/Support/ExceptionToResponseExtensions/ValidationExceptionToResponseExtension.php index 3b14c5a9..fcf58108 100644 --- a/src/Support/ExceptionToResponseExtensions/ValidationExceptionToResponseExtension.php +++ b/src/Support/ExceptionToResponseExtensions/ValidationExceptionToResponseExtension.php @@ -46,6 +46,10 @@ public function toResponse(Type $type) public function reference(ObjectType $type) { - return new Reference('responses', Str::start($type->name, '\\'), $this->components); + return app(Reference::class, [ + 'referenceType' => 'responses', + 'fullName' => Str::start($type->name, '\\'), + 'components' => $this->components, + ]); } } diff --git a/src/Support/Generator/Components.php b/src/Support/Generator/Components.php index 3f5f0d6e..7e368b00 100644 --- a/src/Support/Generator/Components.php +++ b/src/Support/Generator/Components.php @@ -35,7 +35,11 @@ public function addSchema(string $schemaName, Schema $schema): Reference { $this->schemas[$schemaName] = $schema; - return new Reference('schemas', $schemaName, $this); + return app(Reference::class, [ + 'referenceType' => 'schemas', + 'fullName' => $schemaName, + 'components' => $this, + ]); } public function removeSchema(string $schemaName): void @@ -95,7 +99,11 @@ public function uniqueSchemaName(string $fullName) public function getSchemaReference(string $schemaName) { - return new Reference('schemas', $schemaName, $this); + return app(Reference::class, [ + 'referenceType' => 'schemas', + 'fullName' => $schemaName, + 'components' => $this, + ]); } public function getSchema(string $schemaName) diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index f80636c9..984a835b 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -30,13 +30,13 @@ */ class TypeTransformer { - private Infer $infer; + protected Infer $infer; - private Components $components; + protected Components $components; - private array $typeToSchemaExtensions; + protected array $typeToSchemaExtensions; - private array $exceptionToResponseExtensions; + protected array $exceptionToResponseExtensions; public function __construct( Infer $infer, @@ -221,7 +221,7 @@ public function transform(Type $type) return $openApiType; } - private function handleUsingExtensions(Type $type) + protected function handleUsingExtensions(Type $type) { return array_reduce( $this->typeToSchemaExtensions, @@ -313,7 +313,7 @@ public function toResponse(Type $type) return $response; } - private function handleResponseUsingExtensions(Type $type) + protected function handleResponseUsingExtensions(Type $type) { if (! $type->isInstanceOf(\Throwable::class)) { return array_reduce( diff --git a/src/Support/OperationExtensions/RequestBodyExtension.php b/src/Support/OperationExtensions/RequestBodyExtension.php index d3758e57..b5a5783e 100644 --- a/src/Support/OperationExtensions/RequestBodyExtension.php +++ b/src/Support/OperationExtensions/RequestBodyExtension.php @@ -110,7 +110,11 @@ protected function addRequestBody(Operation $operation, string $mediaType, Schem $operation->addRequestBodyObject( RequestBodyObject::make()->setContent( $mediaType, - new Reference('schemas', $schemaName, $components), + app(Reference::class, [ + 'referenceType' => 'schemas', + 'fullName' => $schemaName, + 'components' => $components, + ]), ) ); } diff --git a/src/Support/TypeToSchemaExtensions/EnumToSchema.php b/src/Support/TypeToSchemaExtensions/EnumToSchema.php index e2910efe..51375ec2 100644 --- a/src/Support/TypeToSchemaExtensions/EnumToSchema.php +++ b/src/Support/TypeToSchemaExtensions/EnumToSchema.php @@ -40,6 +40,10 @@ public function toSchema(Type $type) public function reference(ObjectType $type) { - return new Reference('schemas', $type->name, $this->components); + return app(Reference::class, [ + 'referenceType' => 'schemas', + 'fullName' => $type->name, + 'components' => $this->components, + ]); } } diff --git a/src/Support/TypeToSchemaExtensions/JsonResourceTypeToSchema.php b/src/Support/TypeToSchemaExtensions/JsonResourceTypeToSchema.php index 67ad95d8..0b4034d5 100644 --- a/src/Support/TypeToSchemaExtensions/JsonResourceTypeToSchema.php +++ b/src/Support/TypeToSchemaExtensions/JsonResourceTypeToSchema.php @@ -148,7 +148,11 @@ private function transformNullableType(?KeyedArrayType $type) public function reference(ObjectType $type) { - return new Reference('schemas', $type->name, $this->components); + return app(Reference::class, [ + 'referenceType' => 'schemas', + 'fullName' => $type->name, + 'components' => $this->components, + ]); /* * @todo: Allow (enforce) user to explicitly pass short and unique names for the reference and avoid passing components. diff --git a/src/Support/TypeToSchemaExtensions/ModelToSchema.php b/src/Support/TypeToSchemaExtensions/ModelToSchema.php index e06aa3e7..2d2d2eb8 100644 --- a/src/Support/TypeToSchemaExtensions/ModelToSchema.php +++ b/src/Support/TypeToSchemaExtensions/ModelToSchema.php @@ -46,6 +46,10 @@ public function toResponse(Type $type) public function reference(ObjectType $type) { - return new Reference('schemas', $type->name, $this->components); + return app(Reference::class, [ + 'referenceType' => 'schemas', + 'fullName' => $type->name, + 'components' => $this->components, + ]); } } From 5547a2a0cfec7420307190f790b7cad3394d62ac Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Tue, 17 Sep 2024 13:44:21 +0300 Subject: [PATCH 09/34] feat(param-references): Add ability to include params as references --- src/Support/Generator/Operation.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Support/Generator/Operation.php b/src/Support/Generator/Operation.php index 5e01fa24..5deace6a 100644 --- a/src/Support/Generator/Operation.php +++ b/src/Support/Generator/Operation.php @@ -160,7 +160,10 @@ public function toArray() } if (count($this->parameters)) { - $result['parameters'] = array_map(fn (Parameter $p) => $p->toArray(), $this->parameters); + $result['parameters'] = array_map( + static fn (Parameter|Reference $p) => $p->toArray(), + $this->parameters, + ); } if ($this->requestBodyObject) { From a1e68d23e2958d0ae00eb1c691caa9ae02d113a7 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Wed, 18 Sep 2024 13:10:02 +0300 Subject: [PATCH 10/34] feat(enum-tag): Add @enum tag support --- src/Support/Generator/TypeTransformer.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 984a835b..09d7effb 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -128,6 +128,17 @@ public function transform(Type $type) ? $this->transform(PhpDocTypeHelper::toType($varNode->type)) : $openApiType; + $enumNode = array_values($docNode->getTagsByName('@enum'))[0] ?? null; + if ($enumNode) { + $values = explode('|', $enumNode->value->value); + + if (is_int($values[0])) { + $openApiType = (new IntegerType())->enum($values); + } else { + $openApiType = (new StringType())->enum($values); + } + } + $commentDescription = trim($docNode->getAttribute('summary') . ' ' . $docNode->getAttribute('description')); $varNodeDescription = $varNode && $varNode->description ? trim($varNode->description) : ''; if ($commentDescription || $varNodeDescription) { From f59d7d045f60b406b5dc7f75adddc357f5d9fadf Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Thu, 19 Sep 2024 13:38:07 +0300 Subject: [PATCH 11/34] feat(type-ad-hoc-description): Add type ad-hoc description support --- src/Support/Generator/TypeTransformer.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 09d7effb..301409a1 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -229,6 +229,10 @@ public function transform(Type $type) $openApiType->setAttribute('line', $type->getAttribute('line')); } + if ($description = $type->getAttribute('description')) { + $openApiType->setDescription($description); + } + return $openApiType; } From b711602fabdbe7e28d3e7a57c998ff8d32277efc Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Thu, 19 Sep 2024 13:38:36 +0300 Subject: [PATCH 12/34] feat(type-ad-hoc-description): Add type ad-hoc description support --- src/Support/Generator/TypeTransformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 301409a1..53fec31f 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -229,7 +229,7 @@ public function transform(Type $type) $openApiType->setAttribute('line', $type->getAttribute('line')); } - if ($description = $type->getAttribute('description')) { + if (! $openApiType->description && $description = $type->getAttribute('description')) { $openApiType->setDescription($description); } From 69faabe517c9c77a3058ae6d6ba7cbdbe8a40f48 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Thu, 19 Sep 2024 14:44:07 +0300 Subject: [PATCH 13/34] feat(type-ad-hoc-default): Add ad-hoc default values support --- src/Support/Generator/TypeTransformer.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 53fec31f..ac856c2e 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -229,6 +229,15 @@ public function transform(Type $type) $openApiType->setAttribute('line', $type->getAttribute('line')); } + if ($type->hasAttribute('default')) { + $openApiType->default($type->getAttribute('default')); + } + + if ($openApiType->default && ! $openApiType->default instanceof MissingExample) { + $openApiType->example = new MissingExample(); + $openApiType->examples = []; + } + if (! $openApiType->description && $description = $type->getAttribute('description')) { $openApiType->setDescription($description); } From 21185966e30051dd93cb21b845254668b65b874a Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Thu, 19 Sep 2024 17:00:12 +0300 Subject: [PATCH 14/34] feat(optional-values): Add optional value automark if it's nullable --- src/Support/Type/ArrayItemType_.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Support/Type/ArrayItemType_.php b/src/Support/Type/ArrayItemType_.php index f02e2a3f..cd3d619c 100644 --- a/src/Support/Type/ArrayItemType_.php +++ b/src/Support/Type/ArrayItemType_.php @@ -25,6 +25,15 @@ public function __construct( $this->value = $value; $this->isOptional = $isOptional; $this->shouldUnpack = $shouldUnpack; + + if (! $this->isOptional && $this->value instanceof Union) { + foreach ($this->value->types as $type) { + if ($type instanceof NullType) { + $this->isOptional = true; + break; + } + } + } } public function nodes(): array From 3e4fa51653fb53e250dcdea019e5abbe269fbcb5 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Thu, 19 Sep 2024 17:32:19 +0300 Subject: [PATCH 15/34] feat(optional-values): Add optional value automark if it's nullable --- src/Support/Generator/TypeTransformer.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index ac856c2e..35fd410b 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -94,12 +94,14 @@ public function transform(Type $type) $props = collect($type->items) ->mapWithKeys(function (ArrayItemType_ $item) use (&$requiredKeys) { - if (! $item->isOptional) { + $value = $this->transform($item); + + if (! $value->nullable) { $requiredKeys[] = $item->key; } return [ - $item->key => $this->transform($item), + $item->key => $value, ]; }); From a2465ddf7f0cf33c90fe9aa51932ce8054e3b26f Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Thu, 19 Sep 2024 18:56:55 +0300 Subject: [PATCH 16/34] feat(arrqay-items): Add array items support, remove hadrcode for additional items in lists --- src/Support/Generator/TypeTransformer.php | 5 ++-- src/Support/Generator/Types/ArrayType.php | 34 +++++++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 35fd410b..1a29eda0 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -77,14 +77,13 @@ public function transform(Type $type) /** @see https://stackoverflow.com/questions/57464633/how-to-define-a-json-array-with-concrete-item-definition-for-every-index-i-e-a */ $openApiType = (new ArrayType()) ->setMin(count($type->items)) - ->setMax(count($type->items)) - ->setPrefixItems( + ->setItems( array_map( fn($item) => $this->transform($item->value), $type->items, ), ) - ->setAdditionalItems(false); + ->setAdditionalItems(true); } elseif ( $type instanceof \Dedoc\Scramble\Support\Type\KeyedArrayType && ! $type->isList diff --git a/src/Support/Generator/Types/ArrayType.php b/src/Support/Generator/Types/ArrayType.php index 0f864e25..d620070b 100644 --- a/src/Support/Generator/Types/ArrayType.php +++ b/src/Support/Generator/Types/ArrayType.php @@ -6,10 +6,10 @@ class ArrayType extends Type { - /** @var Type|Schema */ + /** @var Type|Schema|array */ public $items; - /** @var Type|Schema */ + /** @var Type|Schema|array */ public $prefixItems = []; public $minItems = null; @@ -22,7 +22,7 @@ public function __construct() { parent::__construct('array'); - $defaultMissingType = new StringType; + $defaultMissingType = new StringType(); $defaultMissingType->setAttribute('missing', true); $this->items = $defaultMissingType; @@ -65,22 +65,32 @@ public function setAdditionalItems($additionalItems) public function toArray() { - $shouldOmitItems = $this->items->getAttribute('missing') + $shouldOmitItems = ! is_array($this->items) + && $this->items->getAttribute('missing') && count($this->prefixItems); return array_merge( parent::toArray(), - $shouldOmitItems ? [] : [ - 'items' => $this->items->toArray(), + $shouldOmitItems + ? [] + : [ + 'items' => is_array($this->items) + ? array_map(static fn($item) => $item->toArray(), $this->items) + : $this->items->toArray(), ], $this->prefixItems ? [ - 'prefixItems' => array_map(fn ($item) => $item->toArray(), $this->prefixItems), + 'prefixItems' => is_array($this->prefixItems) + ? array_map(static fn($item) => $item->toArray(), $this->prefixItems) + : $this->prefixItems->toArray(), ] : [], - array_filter([ - 'minItems' => $this->minItems, - 'maxItems' => $this->maxItems, - 'additionalItems' => $this->additionalItems, - ], fn ($v) => $v !== null) + array_filter( + [ + 'minItems' => $this->minItems, + 'maxItems' => $this->maxItems, + 'additionalItems' => $this->additionalItems, + ], + static fn($v) => $v !== null, + ), ); } } From 1ebf540a844d72f8a2e0eaa0c814a9b03a12da5c Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Thu, 19 Sep 2024 18:57:45 +0300 Subject: [PATCH 17/34] feat(array-items): Add array items support, remove hadrcode for additional items in lists --- src/Support/Generator/TypeTransformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 1a29eda0..996560f1 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -76,7 +76,7 @@ public function transform(Type $type) ) { /** @see https://stackoverflow.com/questions/57464633/how-to-define-a-json-array-with-concrete-item-definition-for-every-index-i-e-a */ $openApiType = (new ArrayType()) - ->setMin(count($type->items)) + ->setMin(1) ->setItems( array_map( fn($item) => $this->transform($item->value), From a332108800c2d46de802416c7323252beea31ced Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Thu, 19 Sep 2024 18:58:06 +0300 Subject: [PATCH 18/34] feat(array-items): Add array items support, remove hadrcode for additional items in lists --- src/Support/Generator/TypeTransformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 996560f1..1a29eda0 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -76,7 +76,7 @@ public function transform(Type $type) ) { /** @see https://stackoverflow.com/questions/57464633/how-to-define-a-json-array-with-concrete-item-definition-for-every-index-i-e-a */ $openApiType = (new ArrayType()) - ->setMin(1) + ->setMin(count($type->items)) ->setItems( array_map( fn($item) => $this->transform($item->value), From 15fa8065350eafa49f5b322feb746d891560e52d Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Thu, 19 Sep 2024 21:22:38 +0300 Subject: [PATCH 19/34] feat(array-object): Add array-object type support --- src/Support/Generator/TypeTransformer.php | 23 +++++++++++++++++-- .../Generator/Types/ArrayObjectType.php | 13 +++++++++++ src/Support/Generator/Types/ObjectType.php | 4 ++-- src/Support/Type/ArrayObjectType.php | 9 ++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 src/Support/Generator/Types/ArrayObjectType.php create mode 100644 src/Support/Type/ArrayObjectType.php diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 1a29eda0..29002128 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -6,6 +6,7 @@ use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper; use Dedoc\Scramble\Support\Generator\Combined\AllOf; use Dedoc\Scramble\Support\Generator\Combined\AnyOf; +use Dedoc\Scramble\Support\Generator\Types\ArrayObjectType as OpenApiArrayObjectType; use Dedoc\Scramble\Support\Generator\Types\ArrayType; use Dedoc\Scramble\Support\Generator\Types\BooleanType; use Dedoc\Scramble\Support\Generator\Types\IntegerType; @@ -16,6 +17,7 @@ use Dedoc\Scramble\Support\Generator\Types\UnknownType; use Dedoc\Scramble\Support\Helpers\ExamplesExtractor; use Dedoc\Scramble\Support\Type\ArrayItemType_; +use Dedoc\Scramble\Support\Type\ArrayObjectType; use Dedoc\Scramble\Support\Type\Literal\LiteralFloatType; use Dedoc\Scramble\Support\Type\Literal\LiteralIntegerType; use Dedoc\Scramble\Support\Type\Literal\LiteralStringType; @@ -77,13 +79,14 @@ public function transform(Type $type) /** @see https://stackoverflow.com/questions/57464633/how-to-define-a-json-array-with-concrete-item-definition-for-every-index-i-e-a */ $openApiType = (new ArrayType()) ->setMin(count($type->items)) - ->setItems( + ->setMax(count($type->items)) + ->setPrefixItems( array_map( fn($item) => $this->transform($item->value), $type->items, ), ) - ->setAdditionalItems(true); + ->setAdditionalItems(false); } elseif ( $type instanceof \Dedoc\Scramble\Support\Type\KeyedArrayType && ! $type->isList @@ -107,6 +110,22 @@ public function transform(Type $type) $openApiType->properties = $props->all(); $openApiType->setRequired($requiredKeys); + } elseif ($type instanceof ArrayObjectType) { + $openApiType = (new OpenApiArrayObjectType()); + $props = collect($type->items) + ->mapWithKeys(function (ArrayItemType_ $item) use (&$requiredKeys) { + $value = $this->transform($item); + + if (! $value->nullable) { + $requiredKeys[] = $item->key; + } + + return [ + $item->key => $value, + ]; + }); + + $openApiType->properties = $props->all(); } elseif ( $type instanceof \Dedoc\Scramble\Support\Type\ArrayType ) { diff --git a/src/Support/Generator/Types/ArrayObjectType.php b/src/Support/Generator/Types/ArrayObjectType.php new file mode 100644 index 00000000..96e670ab --- /dev/null +++ b/src/Support/Generator/Types/ArrayObjectType.php @@ -0,0 +1,13 @@ + Date: Thu, 19 Sep 2024 21:43:59 +0300 Subject: [PATCH 20/34] feat(array-object): Add array-object type support --- src/Support/Generator/TypeTransformer.php | 26 +++++++------------ .../Generator/Types/ArrayObjectType.php | 13 ---------- 2 files changed, 9 insertions(+), 30 deletions(-) delete mode 100644 src/Support/Generator/Types/ArrayObjectType.php diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 29002128..fe5f0396 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -72,7 +72,15 @@ public function transform(Type $type) $type = $type->is; } - if ( + if ($type instanceof ArrayObjectType) { + $openApiType = (new ArrayType()) + ->setItems( + array_map( + fn($item) => $this->transform($item->value), + $type->items, + ), + ); + } elseif ( $type instanceof \Dedoc\Scramble\Support\Type\KeyedArrayType && $type->isList ) { @@ -110,22 +118,6 @@ public function transform(Type $type) $openApiType->properties = $props->all(); $openApiType->setRequired($requiredKeys); - } elseif ($type instanceof ArrayObjectType) { - $openApiType = (new OpenApiArrayObjectType()); - $props = collect($type->items) - ->mapWithKeys(function (ArrayItemType_ $item) use (&$requiredKeys) { - $value = $this->transform($item); - - if (! $value->nullable) { - $requiredKeys[] = $item->key; - } - - return [ - $item->key => $value, - ]; - }); - - $openApiType->properties = $props->all(); } elseif ( $type instanceof \Dedoc\Scramble\Support\Type\ArrayType ) { diff --git a/src/Support/Generator/Types/ArrayObjectType.php b/src/Support/Generator/Types/ArrayObjectType.php deleted file mode 100644 index 96e670ab..00000000 --- a/src/Support/Generator/Types/ArrayObjectType.php +++ /dev/null @@ -1,13 +0,0 @@ - Date: Fri, 20 Sep 2024 00:06:55 +0300 Subject: [PATCH 21/34] feat(array-object): Add array object type support --- .../Generator/Types/ArrayObjectType.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/Support/Generator/Types/ArrayObjectType.php diff --git a/src/Support/Generator/Types/ArrayObjectType.php b/src/Support/Generator/Types/ArrayObjectType.php new file mode 100644 index 00000000..cfa04d76 --- /dev/null +++ b/src/Support/Generator/Types/ArrayObjectType.php @@ -0,0 +1,32 @@ +items || (! is_array($this->items) && $this->items->getAttribute('missing'))) { + return [ + 'items' => $result, + ]; + } + + $items = is_array($this->items) + ? array_map(static fn($item) => $item->toArray(), $this->items) + : $this->items->toArray(); + + + foreach ($items as $item) { + $result->addProperty($item->key, $item->value); + } + + return [ + 'items' => $result, + ]; + } +} From 43c1ebf6b48a13166efed239439dcbb581cbb250 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Fri, 20 Sep 2024 00:32:48 +0300 Subject: [PATCH 22/34] feat(array-object): Add array-object type support --- src/Support/Generator/TypeTransformer.php | 2 +- .../Generator/Types/ArrayObjectType.php | 24 ++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index fe5f0396..4c0688c1 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -73,7 +73,7 @@ public function transform(Type $type) } if ($type instanceof ArrayObjectType) { - $openApiType = (new ArrayType()) + $openApiType = (new OpenApiArrayObjectType()) ->setItems( array_map( fn($item) => $this->transform($item->value), diff --git a/src/Support/Generator/Types/ArrayObjectType.php b/src/Support/Generator/Types/ArrayObjectType.php index cfa04d76..1eed5726 100644 --- a/src/Support/Generator/Types/ArrayObjectType.php +++ b/src/Support/Generator/Types/ArrayObjectType.php @@ -8,11 +8,11 @@ class ArrayObjectType extends ArrayType { public function toArray(): array { - $result = new ObjectType(); - if (! $this->items || (! is_array($this->items) && $this->items->getAttribute('missing'))) { return [ - 'items' => $result, + 'items' => [ + 'type' => 'object', + ], ]; } @@ -20,13 +20,25 @@ public function toArray(): array ? array_map(static fn($item) => $item->toArray(), $this->items) : $this->items->toArray(); - + $props = $required = []; foreach ($items as $item) { - $result->addProperty($item->key, $item->value); + $props = [ + ...$props, + ...$item['properties'], + ]; + + $required = [ + ...$required, + ...array_keys($item['properties']), + ]; } return [ - 'items' => $result, + 'items' => [ + 'type' => 'object', + 'properties' => $props, + 'required' => $required, + ], ]; } } From bb59b8f361126089117a06b97280dfad64f356f1 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Fri, 20 Sep 2024 00:37:34 +0300 Subject: [PATCH 23/34] feat(array-object): Add array-object type support --- src/Support/Generator/Types/ArrayObjectType.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Support/Generator/Types/ArrayObjectType.php b/src/Support/Generator/Types/ArrayObjectType.php index 1eed5726..09e0249f 100644 --- a/src/Support/Generator/Types/ArrayObjectType.php +++ b/src/Support/Generator/Types/ArrayObjectType.php @@ -10,6 +10,7 @@ public function toArray(): array { if (! $this->items || (! is_array($this->items) && $this->items->getAttribute('missing'))) { return [ + 'type' => 'array', 'items' => [ 'type' => 'object', ], @@ -34,6 +35,7 @@ public function toArray(): array } return [ + 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => $props, From f04de8723fd7471408078a04c0206af83ceab3f4 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Fri, 20 Sep 2024 00:41:10 +0300 Subject: [PATCH 24/34] feat(array-object): Add array-object type support --- src/Support/Generator/Types/ArrayObjectType.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Support/Generator/Types/ArrayObjectType.php b/src/Support/Generator/Types/ArrayObjectType.php index 09e0249f..19bfa2be 100644 --- a/src/Support/Generator/Types/ArrayObjectType.php +++ b/src/Support/Generator/Types/ArrayObjectType.php @@ -10,7 +10,7 @@ public function toArray(): array { if (! $this->items || (! is_array($this->items) && $this->items->getAttribute('missing'))) { return [ - 'type' => 'array', + 'type' => $this->type, 'items' => [ 'type' => 'object', ], @@ -35,7 +35,7 @@ public function toArray(): array } return [ - 'type' => 'array', + 'type' => $this->type, 'items' => [ 'type' => 'object', 'properties' => $props, From 3b5b54efa5de6f1c72a4e68f88d39a92db2f4cce Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Fri, 20 Sep 2024 12:55:09 +0300 Subject: [PATCH 25/34] feat(attribute-title): Add attribute title support --- src/Support/Generator/TypeTransformer.php | 4 ++++ src/Support/Generator/Types/ArrayObjectType.php | 10 ++++++---- src/Support/Generator/Types/Type.php | 10 ++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 4c0688c1..932ede38 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -250,6 +250,10 @@ public function transform(Type $type) $openApiType->examples = []; } + if (! $openApiType->title && $title = $type->getAttribute('title')) { + $openApiType->setTitle($title); + } + if (! $openApiType->description && $description = $type->getAttribute('description')) { $openApiType->setDescription($description); } diff --git a/src/Support/Generator/Types/ArrayObjectType.php b/src/Support/Generator/Types/ArrayObjectType.php index 19bfa2be..98685f83 100644 --- a/src/Support/Generator/Types/ArrayObjectType.php +++ b/src/Support/Generator/Types/ArrayObjectType.php @@ -9,12 +9,13 @@ class ArrayObjectType extends ArrayType public function toArray(): array { if (! $this->items || (! is_array($this->items) && $this->items->getAttribute('missing'))) { - return [ + return array_filter([ 'type' => $this->type, + 'title' => $this->title, 'items' => [ 'type' => 'object', ], - ]; + ]); } $items = is_array($this->items) @@ -34,13 +35,14 @@ public function toArray(): array ]; } - return [ + return array_filter([ 'type' => $this->type, + 'title' => $this->title, 'items' => [ 'type' => 'object', 'properties' => $props, 'required' => $required, ], - ]; + ]); } } diff --git a/src/Support/Generator/Types/Type.php b/src/Support/Generator/Types/Type.php index 6aa72f7c..6118cb24 100644 --- a/src/Support/Generator/Types/Type.php +++ b/src/Support/Generator/Types/Type.php @@ -13,6 +13,8 @@ abstract class Type public string $format = ''; + public string $title = ''; + public string $description = ''; /** @var array|scalar|null|MissingExample */ @@ -68,6 +70,7 @@ public function toArray() array_filter([ 'type' => $this->nullable ? [$this->type, 'null'] : $this->type, 'format' => $this->format, + 'title' => $this->title, 'description' => $this->description, 'enum' => count($this->enum) ? $this->enum : null, ]), @@ -82,6 +85,13 @@ public function toArray() ); } + public function setTitle(string $title): Type + { + $this->title = $title; + + return $this; + } + public function setDescription(string $description): Type { $this->description = $description; From ce40ec381965543ac6bb24cdf0b3a6ffe2a5c17a Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Fri, 20 Sep 2024 15:43:27 +0300 Subject: [PATCH 26/34] feat(attribute-title): Add array object attribute title support --- src/Support/Generator/Types/ArrayObjectType.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Support/Generator/Types/ArrayObjectType.php b/src/Support/Generator/Types/ArrayObjectType.php index 98685f83..0ca3f5b8 100644 --- a/src/Support/Generator/Types/ArrayObjectType.php +++ b/src/Support/Generator/Types/ArrayObjectType.php @@ -24,6 +24,10 @@ public function toArray(): array $props = $required = []; foreach ($items as $item) { + if (! isset($itemTitle)) { + $itemTitle = $item['title'] ?? null; + } + $props = [ ...$props, ...$item['properties'], @@ -38,11 +42,12 @@ public function toArray(): array return array_filter([ 'type' => $this->type, 'title' => $this->title, - 'items' => [ + 'items' => array_filter([ 'type' => 'object', + 'title' => $itemTitle, 'properties' => $props, 'required' => $required, - ], + ]), ]); } } From b20634a6b8e2326402b8ca668e80b24a7b3da835 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Fri, 20 Sep 2024 16:53:44 +0300 Subject: [PATCH 27/34] feat(array-object): Improve and optimize the array object type rendering --- src/Support/Generator/TypeTransformer.php | 7 +----- .../Generator/Types/ArrayObjectType.php | 24 +------------------ src/Support/Type/ArrayObjectType.php | 11 ++++++++- 3 files changed, 12 insertions(+), 30 deletions(-) diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 932ede38..23aa38ce 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -74,12 +74,7 @@ public function transform(Type $type) if ($type instanceof ArrayObjectType) { $openApiType = (new OpenApiArrayObjectType()) - ->setItems( - array_map( - fn($item) => $this->transform($item->value), - $type->items, - ), - ); + ->setItems($this->transform($type->value)); } elseif ( $type instanceof \Dedoc\Scramble\Support\Type\KeyedArrayType && $type->isList diff --git a/src/Support/Generator/Types/ArrayObjectType.php b/src/Support/Generator/Types/ArrayObjectType.php index 0ca3f5b8..3f7e2ca1 100644 --- a/src/Support/Generator/Types/ArrayObjectType.php +++ b/src/Support/Generator/Types/ArrayObjectType.php @@ -22,32 +22,10 @@ public function toArray(): array ? array_map(static fn($item) => $item->toArray(), $this->items) : $this->items->toArray(); - $props = $required = []; - foreach ($items as $item) { - if (! isset($itemTitle)) { - $itemTitle = $item['title'] ?? null; - } - - $props = [ - ...$props, - ...$item['properties'], - ]; - - $required = [ - ...$required, - ...array_keys($item['properties']), - ]; - } - return array_filter([ 'type' => $this->type, 'title' => $this->title, - 'items' => array_filter([ - 'type' => 'object', - 'title' => $itemTitle, - 'properties' => $props, - 'required' => $required, - ]), + 'items' => array_filter($items), ]); } } diff --git a/src/Support/Type/ArrayObjectType.php b/src/Support/Type/ArrayObjectType.php index ff8ff380..2b314dad 100644 --- a/src/Support/Type/ArrayObjectType.php +++ b/src/Support/Type/ArrayObjectType.php @@ -4,6 +4,15 @@ namespace Dedoc\Scramble\Support\Type; -class ArrayObjectType extends KeyedArrayType +class ArrayObjectType extends ArrayType { + public function __construct(Type $value) + { + parent::__construct($value); + } + + public function nodes(): array + { + return ['value', 'key']; + } } From 7f4efceab189c1d934de2c7e5f71ffb42248ee59 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Fri, 20 Sep 2024 16:55:47 +0300 Subject: [PATCH 28/34] feat(array-object): Improve and optimize the array object type rendering --- src/Support/Type/ArrayObjectType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/Type/ArrayObjectType.php b/src/Support/Type/ArrayObjectType.php index 2b314dad..169a28fd 100644 --- a/src/Support/Type/ArrayObjectType.php +++ b/src/Support/Type/ArrayObjectType.php @@ -13,6 +13,6 @@ public function __construct(Type $value) public function nodes(): array { - return ['value', 'key']; + return ['value']; } } From 53e19582a453073ddcc846d93ef5fbec137b04c9 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Fri, 20 Sep 2024 17:58:08 +0300 Subject: [PATCH 29/34] resolve: add legacy property fetch type parser --- src/Infer/Scope/Scope.php | 12 ++++++++++++ src/Support/Helpers/JsonResourceHelper.php | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Infer/Scope/Scope.php b/src/Infer/Scope/Scope.php index 34804830..39bc6aaf 100644 --- a/src/Infer/Scope/Scope.php +++ b/src/Infer/Scope/Scope.php @@ -4,6 +4,7 @@ use Dedoc\Scramble\Infer\Definition\ClassDefinition; use Dedoc\Scramble\Infer\Services\FileNameResolver; +use Dedoc\Scramble\Infer\Services\ReferenceTypeResolver; use Dedoc\Scramble\Infer\SimpleTypeGetters\BooleanNotTypeGetter; use Dedoc\Scramble\Infer\SimpleTypeGetters\CastTypeGetter; use Dedoc\Scramble\Infer\SimpleTypeGetters\ClassConstFetchTypeGetter; @@ -305,6 +306,17 @@ public function addVariableType(int $line, string $name, Type $type) $this->variables[$name][] = compact('line', 'type'); } + /** + * @deprecated + */ + public function getPropertyFetchType(Type $calledOn, string $propertyName): Type + { + return (new ReferenceTypeResolver($this->index))->resolve( + $this, + new PropertyFetchReferenceType($calledOn, $propertyName) + ); + } + private function getVariableType(Node\Expr\Variable $node) { $name = (string) $node->name; diff --git a/src/Support/Helpers/JsonResourceHelper.php b/src/Support/Helpers/JsonResourceHelper.php index cb76540d..fce79405 100644 --- a/src/Support/Helpers/JsonResourceHelper.php +++ b/src/Support/Helpers/JsonResourceHelper.php @@ -49,7 +49,8 @@ private static function getModelName(string $jsonResourceClassName, \ReflectionC ->first(fn ($str) => Str::is(['*@property*$resource', '*@mixin*'], $str)); if ($mixinOrPropertyLine) { - $modelName = Str::replace(['@property', '$resource', '@mixin', ' ', '*'], '', $mixinOrPropertyLine); + $modelName = Str::replace(['@property-read', '$resource', '@mixin', ' ', '*'], '', $mixinOrPropertyLine); + $modelName = Str::replace('@property','', $modelName); $modelClass = $getFqName($modelName); From e949994090aeb2626a23e91e02b8e229e0e1d119 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Wed, 25 Sep 2024 17:58:32 +0300 Subject: [PATCH 30/34] feat(empty-request-body): Add empty request body support regardless of method --- src/Support/OperationExtensions/RequestBodyExtension.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Support/OperationExtensions/RequestBodyExtension.php b/src/Support/OperationExtensions/RequestBodyExtension.php index b5a5783e..505c4299 100644 --- a/src/Support/OperationExtensions/RequestBodyExtension.php +++ b/src/Support/OperationExtensions/RequestBodyExtension.php @@ -66,13 +66,7 @@ public function handle(Operation $operation, RouteInfo $routeInfo) $mediaType = $this->getMediaType($operation, $routeInfo, $allParams); if (empty($allParams)) { - if (! in_array($operation->method, static::HTTP_METHODS_WITHOUT_REQUEST_BODY)) { - $operation - ->addRequestBodyObject( - RequestBodyObject::make()->setContent($mediaType, Schema::fromType(new ObjectType)) - ); - } - + // no request params - no request body, regardless of method return; } From 534a6ed761dea458c35068049ffec7cb4540565f Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Tue, 8 Oct 2024 13:54:36 +0300 Subject: [PATCH 31/34] feat(tweak-file-upload): Add file upload in OAS 3.1 format description support --- src/Support/Generator/Types/Type.php | 19 +++++++++++++++++++ .../RulesExtractor/RulesMapper.php | 4 +++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Support/Generator/Types/Type.php b/src/Support/Generator/Types/Type.php index 6118cb24..a354898a 100644 --- a/src/Support/Generator/Types/Type.php +++ b/src/Support/Generator/Types/Type.php @@ -17,6 +17,9 @@ abstract class Type public string $description = ''; + public string $contentMediaType = ''; + public string $contentEncoding = ''; + /** @var array|scalar|null|MissingExample */ public $example; @@ -51,6 +54,20 @@ public function format(string $format) return $this; } + public function contentMediaType(string $format) + { + $this->contentMediaType = $format; + + return $this; + } + + public function contentEncoding(string $format) + { + $this->contentEncoding = $format; + + return $this; + } + public function addProperties(Type $fromType) { $this->attributes = $fromType->attributes; @@ -70,6 +87,8 @@ public function toArray() array_filter([ 'type' => $this->nullable ? [$this->type, 'null'] : $this->type, 'format' => $this->format, + 'contentMediaType' => $this->contentMediaType, + 'contentEncoding' => $this->contentEncoding, 'title' => $this->title, 'description' => $this->description, 'enum' => count($this->enum) ? $this->enum : null, diff --git a/src/Support/OperationExtensions/RulesExtractor/RulesMapper.php b/src/Support/OperationExtensions/RulesExtractor/RulesMapper.php index edb010da..753e024f 100644 --- a/src/Support/OperationExtensions/RulesExtractor/RulesMapper.php +++ b/src/Support/OperationExtensions/RulesExtractor/RulesMapper.php @@ -177,7 +177,9 @@ public function file(Type $type) $type = $this->string($type); } - return $type->format('binary'); + return $type + ->format('binary') + ->contentMediaType('application/octet-stream'); } public function url(Type $type) From da60531ec78004b136bb2182a20c8a47a8d2bb85 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Wed, 9 Oct 2024 14:25:01 +0300 Subject: [PATCH 32/34] feat(default-values-from-docnode): Add default values from the docnode support --- src/Support/Generator/TypeTransformer.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 10a6490c..7f454fe5 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -158,6 +158,11 @@ public function transform(Type $type) $openApiType->examples($examples); } + if ($default = ExamplesExtractor::make($docNode, '@default') + ->extract(preferString: $openApiType instanceof StringType)) { + $openApiType->default($default[0]); + } + if ($format = array_values($docNode->getTagsByName('@format'))[0]->value->value ?? null) { $openApiType->format($format); } From 0bac1cd8eb91b1d2efa70099013e8b2fd03e9d21 Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Wed, 9 Oct 2024 19:49:31 +0300 Subject: [PATCH 33/34] feat(update-scramble): Use new scramble features --- src/Support/OperationExtensions/RequestBodyExtension.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Support/OperationExtensions/RequestBodyExtension.php b/src/Support/OperationExtensions/RequestBodyExtension.php index 05161d3a..6ff044c3 100644 --- a/src/Support/OperationExtensions/RequestBodyExtension.php +++ b/src/Support/OperationExtensions/RequestBodyExtension.php @@ -196,9 +196,10 @@ protected function extractRouteRequestValidationRules(RouteInfo $routeInfo, $met /** * @param ParametersExtractionResult[] $rulesExtractedResults */ - protected function mergeExtractedProperties(array $rulesExtractedResults, - ParametersExtractionResult $methodCallsExtractedResult) - { + protected function mergeExtractedProperties( + array $rulesExtractedResults, + ParametersExtractionResult $methodCallsExtractedResult, + ) { $rulesParameters = collect($rulesExtractedResults)->flatMap->parameters->keyBy('name'); $methodCallsExtractedResult->parameters = collect($methodCallsExtractedResult->parameters) From 06c2f6c250effcea015319c06e9d583bdbda162c Mon Sep 17 00:00:00 2001 From: Yaroslav Voitenko Date: Fri, 13 Dec 2024 12:15:17 +0200 Subject: [PATCH 34/34] chore: composer dependencies --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index cb328870..b3a3218e 100644 --- a/composer.json +++ b/composer.json @@ -12,9 +12,9 @@ "php": "^8.1", "illuminate/contracts": "^10.0|^11.0", "myclabs/deep-copy": "^1.12", - "nikic/php-parser": "^5.0", + "nikic/php-parser": "^5.3", "phpstan/phpdoc-parser": "^1.0", - "spatie/laravel-package-tools": "^1.9.2" + "spatie/laravel-package-tools": "^1.17.0" }, "require-dev": { "laravel/pint": "^v1.1.0",