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", diff --git a/src/Generator.php b/src/Generator.php index bb480c0e..365f33c3 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) + ->replaceFirst(rtrim($config->get('api_path', 'api'), '/') . '/', '/') + ->trim('/'), + )->addOperation($operation), )) ->toArray(); @@ -90,35 +95,36 @@ public function __invoke(?GeneratorConfig $config = null) $afterOpenApiGenerated($openApi); } - return $openApi->toArray(); + return $openApi; } - private function makeOpenApi(GeneratorConfig $config) + protected function makeOpenApi(GeneratorConfig $config) { $openApi = OpenApi::make('3.1.0') ->setComponents($this->transformer->getComponents()) ->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), ); } return $openApi; } - private function getRoutes(GeneratorConfig $config): Collection + protected function getRoutes(GeneratorConfig $config): Collection { return collect(RouteFacade::getRoutes()) ->pipe(function (Collection $c) { @@ -145,11 +151,11 @@ private 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(); } - 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 +170,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 +189,16 @@ 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)]); + $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()) { @@ -200,9 +208,9 @@ private 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) { @@ -217,9 +225,9 @@ private function moveSameAlternativeServersToPath(OpenApi $openApi) } } - private function setUniqueOperationId(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')); @@ -234,7 +242,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 7fe94f23..d1165f4b 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,9 +91,11 @@ 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, + annotations: $annotations, + attributes: $attrs, ); /* @@ -79,12 +111,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, @@ -94,15 +126,11 @@ 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, + returnType: new UnknownType(), ), definingClassName: $name, ); @@ -110,7 +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/Infer/Definition/ClassDefinition.php b/src/Infer/Definition/ClassDefinition.php index 4940d18a..b09b4492 100644 --- a/src/Infer/Definition/ClassDefinition.php +++ b/src/Infer/Definition/ClassDefinition.php @@ -31,6 +31,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/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/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/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/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) { 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); diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 693c2bd8..79d16839 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; @@ -30,19 +32,19 @@ */ 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, Components $components, array $typeToSchemaExtensions = [], - array $exceptionToResponseExtensions = [] + array $exceptionToResponseExtensions = [], ) { $this->infer = $infer; $this->components = $components; @@ -50,6 +52,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,42 +66,47 @@ public function getComponents(): Components public function transform(Type $type) { - $openApiType = new UnknownType; + $openApiType = new UnknownType(); if ($type instanceof TemplateType && $type->is) { $type = $type->is; } - if ( + if ($type instanceof ArrayObjectType) { + $openApiType = (new OpenApiArrayObjectType()) + ->setItems($this->transform($type->value)); + } elseif ( $type instanceof \Dedoc\Scramble\Support\Type\KeyedArrayType && $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) ->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, ]; }); @@ -105,9 +119,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 +135,26 @@ public function transform(Type $type) ? $this->transform(PhpDocTypeHelper::toType($varNode->type)) : $openApiType; - $commentDescription = trim($docNode->getAttribute('summary').' '.$docNode->getAttribute('description')); + $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) { - $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); } @@ -140,59 +167,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()->values()->toArray() + $items[] = (new StringType())->enum( + $stringLiterals->map->value->unique()->values()->toArray(), ); } if ($integerLiterals->count()) { - $items[] = (new IntegerType)->enum( - $integerLiterals->map->value->unique()->values()->toArray() + $items[] = (new IntegerType())->enum( + $integerLiterals->map->value->unique()->values()->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, ))); } @@ -213,10 +240,27 @@ 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->title && $title = $type->getAttribute('title')) { + $openApiType->setTitle($title); + } + + if (! $openApiType->description && $description = $type->getAttribute('description')) { + $openApiType->setDescription($description); + } + return $openApiType; } - private function handleUsingExtensions(Type $type) + protected function handleUsingExtensions(Type $type) { return array_reduce( $this->typeToSchemaExtensions, @@ -237,7 +281,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)) { @@ -256,7 +301,7 @@ function ($acc, $extensionClass) use ($type) { } return $acc; - } + }, ); } @@ -265,7 +310,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); } @@ -277,15 +325,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); @@ -304,7 +352,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( @@ -321,7 +369,7 @@ function ($acc, $extensionClass) use ($type) { } return $acc; - } + }, ); } @@ -352,7 +400,7 @@ function ($acc, $extensionClass) use ($type) { } return $acc; - } + }, ); } } diff --git a/src/Support/Generator/Types/ArrayObjectType.php b/src/Support/Generator/Types/ArrayObjectType.php new file mode 100644 index 00000000..3f7e2ca1 --- /dev/null +++ b/src/Support/Generator/Types/ArrayObjectType.php @@ -0,0 +1,31 @@ +items || (! is_array($this->items) && $this->items->getAttribute('missing'))) { + return array_filter([ + 'type' => $this->type, + 'title' => $this->title, + 'items' => [ + 'type' => 'object', + ], + ]); + } + + $items = is_array($this->items) + ? array_map(static fn($item) => $item->toArray(), $this->items) + : $this->items->toArray(); + + return array_filter([ + 'type' => $this->type, + 'title' => $this->title, + 'items' => array_filter($items), + ]); + } +} 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, + ), ); } } diff --git a/src/Support/Generator/Types/ObjectType.php b/src/Support/Generator/Types/ObjectType.php index 9426f3df..e48c61f7 100644 --- a/src/Support/Generator/Types/ObjectType.php +++ b/src/Support/Generator/Types/ObjectType.php @@ -12,9 +12,9 @@ class ObjectType extends Type public ?Type $additionalProperties = null; - public function __construct() + public function __construct($type = null) { - parent::__construct('object'); + parent::__construct($type ?? 'object'); } public function addProperty(string $name, $propertyType) diff --git a/src/Support/Generator/Types/Type.php b/src/Support/Generator/Types/Type.php index 6aa72f7c..a354898a 100644 --- a/src/Support/Generator/Types/Type.php +++ b/src/Support/Generator/Types/Type.php @@ -13,8 +13,13 @@ abstract class Type public string $format = ''; + public string $title = ''; + public string $description = ''; + public string $contentMediaType = ''; + public string $contentEncoding = ''; + /** @var array|scalar|null|MissingExample */ public $example; @@ -49,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; @@ -68,6 +87,9 @@ 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, ]), @@ -82,6 +104,13 @@ public function toArray() ); } + public function setTitle(string $title): Type + { + $this->title = $title; + + return $this; + } + public function setDescription(string $description): Type { $this->description = $description; 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); diff --git a/src/Support/OperationExtensions/RequestBodyExtension.php b/src/Support/OperationExtensions/RequestBodyExtension.php index d8d9ddb5..6ff044c3 100644 --- a/src/Support/OperationExtensions/RequestBodyExtension.php +++ b/src/Support/OperationExtensions/RequestBodyExtension.php @@ -43,7 +43,7 @@ public function handle(Operation $operation, RouteInfo $routeInfo) if (app()->environment('testing')) { throw $exception; } - $description = $description->append('⚠️Cannot generate request documentation: '.$exception->getMessage()); + $description = $description->append('⚠️Cannot generate request documentation: ' . $exception->getMessage()); } $operation @@ -53,12 +53,13 @@ public function handle(Operation $operation, RouteInfo $routeInfo) $allParams = $rulesResults->flatMap->parameters->unique('name')->values()->all(); [$queryParams, $bodyParams] = collect($allParams) - ->partition(fn (Parameter $p) => $p->getAttribute('isInQuery')) + ->partition(fn(Parameter $p) => $p->getAttribute('isInQuery')) ->map->toArray(); $mediaType = $this->getMediaType($operation, $routeInfo, $allParams); if (empty($allParams)) { + // no request params - no request body, regardless of method return; } @@ -73,11 +74,11 @@ public function handle(Operation $operation, RouteInfo $routeInfo) $schemalessResults = collect([$this->mergeSchemalessRulesResults($schemalessResults->values())]); $schemas = $schemaResults->merge($schemalessResults) - ->filter(fn (ParametersExtractionResult $r) => count($r->parameters) || $r->schemaName) + ->filter(fn(ParametersExtractionResult $r) => count($r->parameters) || $r->schemaName) ->map(function (ParametersExtractionResult $r) use ($queryParams) { $qpNames = collect($queryParams)->keyBy('name'); - $r->parameters = collect($r->parameters)->filter(fn ($p) => ! $qpNames->has($p->name))->values()->all(); + $r->parameters = collect($r->parameters)->filter(fn($p) => ! $qpNames->has($p->name))->values()->all(); return $r; }) @@ -122,7 +123,7 @@ protected function makeComposedRequestBodySchema(Collection $schemas) return $schemas->first(); } - return (new AllOf)->setItems($schemas->all()); + return (new AllOf())->setItems($schemas->all()); } protected function mergeSchemalessRulesResults(Collection $schemalessResults): ParametersExtractionResult @@ -168,12 +169,16 @@ protected function extractRouteRequestValidationRules(RouteInfo $routeInfo, $met */ $typeDefiningHandlers = [ new FormRequestRulesExtractor($methodNode, $this->openApiTransformer), - new ValidateCallExtractor($methodNode, $this->openApiTransformer), + new ValidateCallExtractor( + $methodNode, + $this->openApiTransformer, + $routeInfo->route, + ), ]; $validationRulesExtractedResults = collect($typeDefiningHandlers) - ->filter(fn ($h) => $h->shouldHandle()) - ->map(fn ($h) => $h->extract($routeInfo)) + ->filter(static fn($h) => $h->shouldHandle()) + ->map(static fn($h) => $h->extract($routeInfo)) ->values() ->toArray(); @@ -181,7 +186,7 @@ protected function extractRouteRequestValidationRules(RouteInfo $routeInfo, $met * This is the extractor that cannot re-define the incoming type but can add new properties. * Also, it is useful for additional details. */ - $detailsExtractor = new RequestMethodCallsExtractor; + $detailsExtractor = new RequestMethodCallsExtractor(); $methodCallsExtractedResults = $detailsExtractor->extract($routeInfo); @@ -189,21 +194,19 @@ protected function extractRouteRequestValidationRules(RouteInfo $routeInfo, $met } /** - * @param ParametersExtractionResult[] $rulesExtractedResults + * @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) - ->filter(fn (Parameter $p) => ! $rulesParameters->has($p->name)) + ->filter(fn(Parameter $p) => ! $rulesParameters->has($p->name)) ->values() ->all(); - /* - * Possible improvements here: using defaults when merging results, etc. - */ - return [...$rulesExtractedResults, $methodCallsExtractedResult]; } } 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); 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) diff --git a/src/Support/OperationExtensions/RulesExtractor/RulesToParameters.php b/src/Support/OperationExtensions/RulesExtractor/RulesToParameters.php index 2813fbfb..c2575575 100644 --- a/src/Support/OperationExtensions/RulesExtractor/RulesToParameters.php +++ b/src/Support/OperationExtensions/RulesExtractor/RulesToParameters.php @@ -31,7 +31,7 @@ public function __construct(array $rules, array $validationNodesResults, TypeTra $this->nodeDocs = $this->extractNodeDocs($validationNodesResults); } - public function handle() + public function handle(): array { return collect($this->rules) ->pipe($this->handleConfirmed(...)) diff --git a/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php b/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php index 48aa5259..3b924b67 100644 --- a/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php +++ b/src/Support/OperationExtensions/RulesExtractor/ValidateCallExtractor.php @@ -6,6 +6,7 @@ use Dedoc\Scramble\Support\RouteInfo; use Dedoc\Scramble\Support\SchemaClassDocReflector; use Illuminate\Http\Request; +use Illuminate\Routing\Route; use PhpParser\Node; use PhpParser\NodeFinder; use PhpParser\PrettyPrinter\Standard; @@ -15,7 +16,11 @@ class ValidateCallExtractor implements RulesExtractor { use GeneratesParametersFromRules; - public function __construct(private ?Node\FunctionLike $handle, private TypeTransformer $typeTransformer) {} + public function __construct( + protected ?Node\FunctionLike $handle, + protected TypeTransformer $typeTransformer, + protected ?Route $route = null, + ) {} public function shouldHandle(): bool { @@ -28,40 +33,45 @@ public function extract(RouteInfo $routeInfo): ParametersExtractionResult // $request->validate, when $request is a Request instance /** @var Node\Expr\MethodCall $callToValidate */ - $callToValidate = (new NodeFinder)->findFirst( + $callToValidate = (new NodeFinder())->findFirst( $methodNode, - fn (Node $node) => $node instanceof Node\Expr\MethodCall + fn(Node $node) => $node instanceof Node\Expr\MethodCall && $node->var instanceof Node\Expr\Variable && is_a($this->getPossibleParamType($methodNode, $node->var), Request::class, true) && $node->name instanceof Node\Identifier - && $node->name->name === 'validate' + && $node->name->name === 'validate', ); $validationRules = $callToValidate->args[0] ?? null; if (! $validationRules) { // $this->validate($request, $rules), rules are second param. First should be $request, but no way to check type. So relying on convention. - $callToValidate = (new NodeFinder)->findFirst( + $callToValidate = (new NodeFinder())->findFirst( $methodNode, - fn (Node $node) => $node instanceof Node\Expr\MethodCall + fn(Node $node) => $node instanceof Node\Expr\MethodCall && count($node->args) >= 2 && $node->var instanceof Node\Expr\Variable && $node->var->name === 'this' && $node->name instanceof Node\Identifier && $node->name->name === 'validate' && $node->args[0]->value instanceof Node\Expr\Variable && is_a($this->getPossibleParamType($methodNode, $node->args[0]->value), Request::class, true) - && $node->name->name === 'validate' + && $node->name->name === 'validate', ); $validationRules = $callToValidate->args[1] ?? null; } if (! $validationRules) { // Validator::make($request->...(), $rules), rules are second param. First should be $request, but no way to check type. So relying on convention. - $callToValidate = (new NodeFinder)->findFirst( + $callToValidate = (new NodeFinder())->findFirst( $methodNode, - fn (Node $node) => $node instanceof Node\Expr\StaticCall + fn(Node $node) => $node instanceof Node\Expr\StaticCall && count($node->args) >= 2 - && $node->class instanceof Node\Name && is_a($node->class->toString(), \Illuminate\Support\Facades\Validator::class, true) + && $node->class instanceof Node\Name && is_a($node->class->toString(), + \Illuminate\Support\Facades\Validator::class, + true) && $node->name instanceof Node\Identifier && $node->name->name === 'make' - && $node->args[0]->value instanceof Node\Expr\MethodCall && is_a($this->getPossibleParamType($methodNode, $node->args[0]->value->var), Request::class, true) + && $node->args[0]->value instanceof Node\Expr\MethodCall && is_a($this->getPossibleParamType($methodNode, + $node->args[0]->value->var), + Request::class, + true), ); $validationRules = $callToValidate->args[1] ?? null; } @@ -72,13 +82,14 @@ public function extract(RouteInfo $routeInfo): ParametersExtractionResult $validationRulesNode = $validationRules instanceof Node\Arg ? $validationRules->value : $validationRules; - $phpDocReflector = new SchemaClassDocReflector($callToValidate->getAttribute('parsedPhpDoc', new PhpDocNode([]))); + $phpDocReflector = new SchemaClassDocReflector($callToValidate->getAttribute('parsedPhpDoc', + new PhpDocNode([]))); return new ParametersExtractionResult( parameters: $this->makeParameters( - node: (new NodeFinder)->find( + node: (new NodeFinder())->find( $validationRulesNode instanceof Node\Expr\Array_ ? $validationRulesNode->items : [], - fn (Node $node) => $node instanceof Node\Expr\ArrayItem + fn(Node $node) => $node instanceof Node\Expr\ArrayItem && $node->key instanceof Node\Scalar\String_ && $node->getAttribute('parsedPhpDoc'), ), @@ -98,13 +109,15 @@ public function rules($validationRules): array $methodNode = $this->handle; - $printer = new Standard; + $printer = new Standard(); $validationRulesCode = $printer->prettyPrint([$validationRules]); $injectableParams = collect($methodNode->getParams()) - ->filter(fn (Node\Param $param) => isset($param->type->name)) - ->filter(fn (Node\Param $param) => ! class_exists($className = (string) $param->type) || ! is_a($className, Request::class, true)) - ->filter(fn (Node\Param $param) => isset($param->var->name) && is_string($param->var->name)) + ->filter(fn(Node\Param $param) => isset($param->type->name)) + ->filter(fn(Node\Param $param) => ! class_exists($className = (string) $param->type) || ! is_a($className, + Request::class, + true)) + ->filter(fn(Node\Param $param) => isset($param->var->name) && is_string($param->var->name)) ->mapWithKeys(function (Node\Param $param) { try { $type = (string) $param->type; @@ -127,7 +140,14 @@ public function rules($validationRules): array extract($injectableParams); - return 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;"); + } + + return $rules ?? []; } private function getPossibleParamType(Node\Stmt\ClassMethod $methodNode, Node\Expr\Variable $node): ?string diff --git a/src/Support/ResponseExtractor/ModelInfo.php b/src/Support/ResponseExtractor/ModelInfo.php index 75e3e628..5abe26ef 100644 --- a/src/Support/ResponseExtractor/ModelInfo.php +++ b/src/Support/ResponseExtractor/ModelInfo.php @@ -33,7 +33,7 @@ class ModelInfo ]; public function __construct( - private string $class + private string $class, ) {} public function handle() @@ -64,7 +64,8 @@ public function handle() /** * 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) @@ -77,7 +78,7 @@ protected function getAttributes($model) return collect($columns) ->values() - ->map(fn ($column) => [ + ->map(fn($column) => [ 'driver' => $connection->getDriverName(), 'name' => $column['name'], 'type' => $column['type'], @@ -108,15 +109,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) @@ -127,9 +129,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) { @@ -140,8 +142,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, @@ -160,15 +162,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, ) @@ -182,7 +185,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 { @@ -223,8 +226,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) @@ -243,7 +247,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) @@ -251,15 +256,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) @@ -303,7 +309,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/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 diff --git a/src/Support/Type/ArrayObjectType.php b/src/Support/Type/ArrayObjectType.php new file mode 100644 index 00000000..169a28fd --- /dev/null +++ b/src/Support/Type/ArrayObjectType.php @@ -0,0 +1,18 @@ +is?->isInstanceOf($className); + return (bool) $this->is?->isInstanceOf($className); } public function toString(): string 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 3778ab22..ee512e7d 100644 --- a/src/Support/TypeToSchemaExtensions/JsonResourceTypeToSchema.php +++ b/src/Support/TypeToSchemaExtensions/JsonResourceTypeToSchema.php @@ -183,7 +183,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, + ]); } }