From 820d1dd28b37f36f0c3273065b4a3516b1e27796 Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko Date: Fri, 31 Jan 2025 11:32:14 +0200 Subject: [PATCH] wip --- .../GeneratorConfigCollection.php | 9 +- src/Configuration/OperationTransformers.php | 59 +++++++++ src/Contexts/OperationTransformerContext.php | 20 +++ src/Contracts/OperationTransformer.php | 11 ++ src/Generator.php | 9 +- src/GeneratorConfig.php | 34 +++++ .../ExtensionWrapperTransformer.php | 32 +++++ src/Reflections/ReflectionRoute.php | 122 ++++++++++++++++++ src/ScrambleServiceProvider.php | 25 +++- src/Support/OperationBuilder.php | 32 ++++- 10 files changed, 344 insertions(+), 9 deletions(-) create mode 100644 src/Configuration/OperationTransformers.php create mode 100644 src/Contexts/OperationTransformerContext.php create mode 100644 src/Contracts/OperationTransformer.php create mode 100644 src/OperationTransformers/ExtensionWrapperTransformer.php create mode 100644 src/Reflections/ReflectionRoute.php diff --git a/src/Configuration/GeneratorConfigCollection.php b/src/Configuration/GeneratorConfigCollection.php index a12b1c22..9c2db563 100644 --- a/src/Configuration/GeneratorConfigCollection.php +++ b/src/Configuration/GeneratorConfigCollection.php @@ -9,6 +9,9 @@ class GeneratorConfigCollection { + /** + * @var array + */ private array $apis = []; public function __construct() @@ -20,6 +23,7 @@ private function buildDefaultApiConfiguration(): GeneratorConfig { return (new GeneratorConfig( parametersExtractors: new ParametersExtractors, + operationTransformers: new OperationTransformers, ))->expose( ui: fn (Router $router, $action) => $router->get('docs/api', $action)->name('scramble.docs.ui'), document: fn (Router $router, $action) => $router->get('docs/api.json', $action)->name('scramble.docs.document'), @@ -39,9 +43,8 @@ public function register(string $name, array $config): GeneratorConfig { $this->apis[$name] = $generatorConfig = new GeneratorConfig( config: array_merge(config('scramble') ?: [], $config), - parametersExtractors: isset($this->apis[Scramble::DEFAULT_API]) - ? $this->apis[Scramble::DEFAULT_API]->parametersExtractors - : new ParametersExtractors, + parametersExtractors: $this->apis[Scramble::DEFAULT_API]->parametersExtractors, + operationTransformers: $this->apis[Scramble::DEFAULT_API]->operationTransformers, ); return $generatorConfig; diff --git a/src/Configuration/OperationTransformers.php b/src/Configuration/OperationTransformers.php new file mode 100644 index 00000000..3afcc5e9 --- /dev/null +++ b/src/Configuration/OperationTransformers.php @@ -0,0 +1,59 @@ +appends = array_merge( + $this->appends, + Arr::wrap($transformers) + ); + + return $this; + } + + public function prepend(array|callable|string $transformers) + { + $this->prepends = array_merge( + $this->prepends, + Arr::wrap($transformers) + ); + + return $this; + } + + public function use(array $transformers) + { + $this->transformers = $transformers; + + return $this; + } + + public function all(): array + { + $base = $this->transformers ?: [ + RequestEssentialsExtension::class, + RequestBodyExtension::class, + ResponseExtension::class, + ]; + + return array_values(array_unique([ + ...$this->prepends, + ...$base, + ...$this->appends, + ])); + } +} diff --git a/src/Contexts/OperationTransformerContext.php b/src/Contexts/OperationTransformerContext.php new file mode 100644 index 00000000..61ba2219 --- /dev/null +++ b/src/Contexts/OperationTransformerContext.php @@ -0,0 +1,20 @@ +infer, $typeTransformer); + $reflectionRoute = new ReflectionRoute($route, $this->infer, $typeTransformer); - if (! $routeInfo->isClassBased()) { + if (! $reflectionRoute->isControllerAction()) { return null; } - $operation = $this->operationBuilder->build($routeInfo, $openApi, $config, $typeTransformer); + $operation = $this->operationBuilder->build($reflectionRoute, $openApi, $config, $typeTransformer); $this->ensureSchemaTypes($route, $operation); diff --git a/src/GeneratorConfig.php b/src/GeneratorConfig.php index d0d7c779..7657e1b5 100644 --- a/src/GeneratorConfig.php +++ b/src/GeneratorConfig.php @@ -3,11 +3,14 @@ namespace Dedoc\Scramble; use Closure; +use Dedoc\Scramble\Configuration\OperationTransformers; use Dedoc\Scramble\Configuration\ParametersExtractors; use Illuminate\Routing\Route; use Illuminate\Routing\Router; use Illuminate\Support\Arr; use Illuminate\Support\Str; +use ReflectionFunction; +use ReflectionNamedType; class GeneratorConfig { @@ -26,6 +29,7 @@ public function __construct( private ?Closure $routeResolver = null, private array $afterOpenApiGenerated = [], public readonly ParametersExtractors $parametersExtractors = new ParametersExtractors, + public readonly OperationTransformers $operationTransformers = new OperationTransformers, ) {} public function config(array $config) @@ -102,6 +106,36 @@ public function withParametersExtractors(callable $callback): static return $this; } + public function withOperationTransformers(array|string|callable $cb): static + { + if ($this->isOperationTransformerMapper($cb)) { + $cb($this->operationTransformers); + + return $this; + } + + $cb = function (OperationTransformers $transformers) use ($cb) { + $transformers->append($cb); + }; + + $cb($this->operationTransformers); + + return $this; + } + + private function isOperationTransformerMapper($cb): bool + { + if (! $cb instanceof Closure) { + return false; + } + + $reflection = new ReflectionFunction($cb); + + return count($reflection->getParameters()) === 1 + && $reflection->getParameters()[0]->getType() instanceof ReflectionNamedType + && is_a($reflection->getParameters()[0]->getType()->getName(), OperationTransformers::class, true); + } + public function get(string $key, mixed $default = null) { return Arr::get($this->config, $key, $default); diff --git a/src/OperationTransformers/ExtensionWrapperTransformer.php b/src/OperationTransformers/ExtensionWrapperTransformer.php new file mode 100644 index 00000000..fdfec82b --- /dev/null +++ b/src/OperationTransformers/ExtensionWrapperTransformer.php @@ -0,0 +1,32 @@ +route, $context->reflectionRoute); + + $extensionInstance = new $this->extension( + + ) + } + + + public function handle(Operation $operation, OperationTransformerContext $context) + { + $routeInfo = new RouteInfo($context->route, $this->infer, $this->typeTransformer); + } +} diff --git a/src/Reflections/ReflectionRoute.php b/src/Reflections/ReflectionRoute.php new file mode 100644 index 00000000..788ebdd7 --- /dev/null +++ b/src/Reflections/ReflectionRoute.php @@ -0,0 +1,122 @@ +requestParametersFromCalls = new Bag; + $this->indexBuildingBroker = app(Infer\Extensions\IndexBuildingBroker::class); + } + + public function isControllerAction(): bool + { + return is_string($this->route->getAction('uses')); + } + + public function getControllerClass(): ?string + { + return $this->isControllerAction() + ? explode('@', $this->route->getAction('uses'))[0] + : null; + } + + public function getControllerMethod(): ?string + { + return $this->isControllerAction() + ? explode('@', $this->route->getAction('uses'))[1] + : null; + } + + public function parsePhpDoc(): PhpDocNode + { + if ($this->phpDoc) { + return $this->phpDoc; + } + + if (! $this->getControllerMethodAstNode()) { + return new PhpDocNode([]); + } + + $this->phpDoc = $this->getControllerMethodAstNode()->getAttribute('parsedPhpDoc') ?: new PhpDocNode([]); + + return $this->phpDoc; + } + + public function getControllerMethodAstNode(): ?ClassMethod + { + if ($this->methodNode || ! $this->isControllerAction() || ! $this->getReflectionMethod()) { + return $this->methodNode; + } + + return $this->methodNode = MethodReflector::make(...explode('@', $this->route->getAction('uses'))) + ->getAstNode(); + } + + public function getReflectionMethod(): ?ReflectionMethod + { + if (! $this->isControllerAction()) { + return null; + } + + if (! method_exists($this->getControllerClass(), $this->getControllerMethod())) { + return null; + } + + return (new ReflectionClass($this->getControllerClass())) + ->getMethod($this->getControllerMethod()); + } + + public function getReturnType() + { + return (new RouteResponseTypeRetriever($this))->getResponseType(); + } + + /** + * @todo Maybe better name is needed as this method performs method analysis, indexes building, etc. + */ + public function getMethodType(): ?FunctionType + { + if (! $this->isControllerAction() || ! $this->getReflectionMethod()) { + return null; + } + + if (! $this->methodType) { + $def = $this->infer->analyzeClass($this->getReflectionMethod()->getDeclaringClass()->getName()); + + /* + * Here the final resolution of the method types may happen. + */ + $this->methodType = $def->getMethodDefinition($this->getControllerMethod(), indexBuilders: [ + new RequestParametersBuilder($this->requestParametersFromCalls, $this->typeTransformer), + ...$this->indexBuildingBroker->indexBuilders, + ])->type; + } + + return $this->methodType; + } +} diff --git a/src/ScrambleServiceProvider.php b/src/ScrambleServiceProvider.php index 34d563ff..f92285da 100644 --- a/src/ScrambleServiceProvider.php +++ b/src/ScrambleServiceProvider.php @@ -3,6 +3,7 @@ namespace Dedoc\Scramble; use Dedoc\Scramble\Configuration\GeneratorConfigCollection; +use Dedoc\Scramble\Configuration\OperationTransformers; use Dedoc\Scramble\Configuration\ParametersExtractors; use Dedoc\Scramble\Console\Commands\AnalyzeDocumentation; use Dedoc\Scramble\Console\Commands\ExportDocumentation; @@ -15,6 +16,7 @@ use Dedoc\Scramble\Infer\Extensions\InferExtension; use Dedoc\Scramble\Infer\Scope\Index; use Dedoc\Scramble\Infer\Services\FileParser; +use Dedoc\Scramble\OperationTransformers\ExtensionWrapperTransformer; use Dedoc\Scramble\Support\ExceptionToResponseExtensions\AuthenticationExceptionToResponseExtension; use Dedoc\Scramble\Support\ExceptionToResponseExtensions\AuthorizationExceptionToResponseExtension; use Dedoc\Scramble\Support\ExceptionToResponseExtensions\HttpExceptionToResponseExtension; @@ -216,7 +218,28 @@ public function configurePackage(Package $package): void public function bootingPackage() { - Scramble::configure()->useConfig(config('scramble')); + Scramble::configure() + ->useConfig(config('scramble')) + ->withOperationTransformers(function (OperationTransformers $transformers) { + $extensions = array_merge(config('scramble.extensions', []), Scramble::$extensions); + + $operationExtensions = array_values(array_filter( + $extensions, + fn ($e) => is_a($e, OperationExtension::class, true), + )); + + $operationExtensions = array_merge([ + RequestEssentialsExtension::class, + RequestBodyExtension::class, + ErrorResponsesExtension::class, + ResponseExtension::class, + DeprecationExtension::class, + ], $operationExtensions); + + $transformers->append(array_map(function ($extension) { + return new ExtensionWrapperTransformer($extension); + }, $operationExtensions)); + }); $this->app->booted(function (Application $app) { Scramble::configure() diff --git a/src/Support/OperationBuilder.php b/src/Support/OperationBuilder.php index 5601bb47..f5b17cb4 100644 --- a/src/Support/OperationBuilder.php +++ b/src/Support/OperationBuilder.php @@ -2,8 +2,10 @@ namespace Dedoc\Scramble\Support; +use Dedoc\Scramble\Contexts\OperationTransformerContext; use Dedoc\Scramble\Extensions\OperationExtension; use Dedoc\Scramble\GeneratorConfig; +use Dedoc\Scramble\Reflections\ReflectionRoute; use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\Operation; use Dedoc\Scramble\Support\Generator\TypeTransformer; @@ -19,10 +21,38 @@ public function __construct(array $extensionsClasses = []) $this->extensionsClasses = $extensionsClasses; } - public function build(RouteInfo $routeInfo, OpenApi $openApi, GeneratorConfig $config, TypeTransformer $typeTransformer) + public function build(ReflectionRoute $reflectionRoute, OpenApi $openApi, GeneratorConfig $config, TypeTransformer $typeTransformer) { $operation = new Operation('get'); + $operationTransformerContext = new OperationTransformerContext( + $reflectionRoute->route, + $reflectionRoute, + $openApi, + $config, + ); + + $routeInfo = new RouteInfo($route, $this->infer, $typeTransformer); + + foreach ($config->operationTransformers->all() as $operationTransformer) { + $instance = is_callable($operationTransformer) + ? $operationTransformer + : ContainerUtils::makeContextable($operationTransformer, [ + OpenApi::class => $openApi, + GeneratorConfig::class => $config, + TypeTransformer::class => $typeTransformer, + ]); + + if (is_callable($instance)) { + $instance($operation, $context); + + continue; + } + + $instance->handle($operation, $context); + } + + foreach ($this->extensionsClasses as $extensionClass) { $extension = app()->make($extensionClass, [ 'openApi' => $openApi,