├── .gitattributes ├── LICENSE ├── composer.json ├── publish └── swagger.php └── src ├── Annotation ├── AdditionalProperties.php ├── AnnotationTrait.php ├── Attachable.php ├── Components.php ├── Contact.php ├── CookieParameter.php ├── Delete.php ├── Discriminator.php ├── Examples.php ├── ExternalDocumentation.php ├── Flow.php ├── Get.php ├── Head.php ├── Header.php ├── HeaderParameter.php ├── HyperfServer.php ├── Info.php ├── Items.php ├── JsonContent.php ├── License.php ├── Link.php ├── MediaType.php ├── MultipleAnnotationTrait.php ├── OpenApi.php ├── Options.php ├── Parameter.php ├── Patch.php ├── PathItem.php ├── PathParameter.php ├── Post.php ├── Property.php ├── Put.php ├── QueryParameter.php ├── RequestBody.php ├── Response.php ├── Schema.php ├── SecurityScheme.php ├── Server.php ├── ServerVariable.php ├── Tag.php ├── Trace.php ├── Xml.php └── XmlContent.php ├── Command ├── Ast │ └── ModelSchemaVisitor.php ├── GenCommand.php ├── GenSchemaCommand.php └── stubs │ └── schema.stub ├── ConfigProvider.php ├── Exception └── RuntimeException.php ├── Generator.php ├── HttpServer.php ├── Listener └── BootSwaggerListener.php ├── Processor └── BuildPathsProcessor.php ├── Request ├── RuleCollector.php ├── SwaggerRequest.php └── ValidationCollector.php └── Util.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | /.github export-ignore 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Hyperf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf/swagger", 3 | "description": "A swagger library for Hyperf.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "php", 7 | "swoole", 8 | "hyperf", 9 | "swagger" 10 | ], 11 | "homepage": "https://hyperf.io", 12 | "support": { 13 | "issues": "https://github.com/hyperf/hyperf/issues", 14 | "source": "https://github.com/hyperf/hyperf", 15 | "docs": "https://hyperf.wiki", 16 | "pull-request": "https://github.com/hyperf/hyperf/pulls" 17 | }, 18 | "require": { 19 | "php": ">=8.1", 20 | "hyperf/command": "~3.1.0", 21 | "zircote/swagger-php": "^4.6" 22 | }, 23 | "suggest": { 24 | "hyperf/validation": "Required to use SwaggerRequest." 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Hyperf\\Swagger\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "HyperfTest\\Swagger\\": "tests/" 34 | } 35 | }, 36 | "config": { 37 | "sort-packages": true 38 | }, 39 | "extra": { 40 | "branch-alias": { 41 | "dev-master": "3.1-dev" 42 | }, 43 | "hyperf": { 44 | "config": "Hyperf\\Swagger\\ConfigProvider" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /publish/swagger.php: -------------------------------------------------------------------------------- 1 | true, 14 | 'port' => 9500, 15 | 'json_dir' => BASE_PATH . '/storage/swagger', 16 | 'html' => null, 17 | 'url' => '/swagger', 18 | 'auto_generate' => true, 19 | 'scan' => [ 20 | 'paths' => null, 21 | ], 22 | 'processors' => [ 23 | // users can append their own processors here 24 | ], 25 | 'server' => [ 26 | 'http' => [ 27 | 'servers' => [ 28 | [ 29 | 'url' => 'http://127.0.0.1:9501', 30 | 'description' => 'Test Server', 31 | ], 32 | ], 33 | 'info' => [ 34 | 'title' => 'Sample API', 35 | 'description' => 'This is a sample API using OpenAPI 3.0 specification', 36 | 'version' => '1.0.0', 37 | ], 38 | ], 39 | ], 40 | ]; 41 | -------------------------------------------------------------------------------- /src/Annotation/AdditionalProperties.php: -------------------------------------------------------------------------------- 1 | formatAnnotation($annotation)); 25 | } 26 | 27 | public function collectClassConstant(string $className, ?string $target): void 28 | { 29 | $annotation = AnnotationCollector::getClassConstantAnnotation($className, $target)[static::class] ?? null; 30 | 31 | AnnotationCollector::collectClassConstant($className, $target, static::class, $this->formatAnnotation($annotation)); 32 | } 33 | 34 | public function collectMethod(string $className, ?string $target): void 35 | { 36 | $annotation = AnnotationCollector::getClassMethodAnnotation($className, $target)[static::class] ?? null; 37 | 38 | AnnotationCollector::collectMethod($className, $target, static::class, $this->formatAnnotation($annotation)); 39 | } 40 | 41 | public function collectProperty(string $className, ?string $target): void 42 | { 43 | $annotation = AnnotationCollector::getClassPropertyAnnotation($className, $target)[static::class] ?? null; 44 | 45 | AnnotationCollector::collectProperty($className, $target, static::class, $this->formatAnnotation($annotation)); 46 | } 47 | 48 | protected function formatAnnotation(?MultipleAnnotation $annotation): MultipleAnnotation 49 | { 50 | if ($annotation instanceof MultipleAnnotation) { 51 | return $annotation->insert($this); 52 | } 53 | 54 | return new MultipleAnnotation($this); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Annotation/OpenApi.php: -------------------------------------------------------------------------------- 1 | _content = $content; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Annotation/Response.php: -------------------------------------------------------------------------------- 1 | model->getConnection(); 34 | $builder = $connection->getSchemaBuilder(); 35 | 36 | $this->columns = array_filter($builder->getColumns(), function (Column $column) { 37 | return $column->getTable() === $this->model->getTable(); 38 | }); 39 | } 40 | 41 | public function beforeTraverse(array $nodes) 42 | { 43 | return null; 44 | } 45 | 46 | public function leaveNode(Node $node) 47 | { 48 | switch ($node) { 49 | case $node instanceof Node\Stmt\Class_: 50 | $node->stmts = array_merge([], $this->buildProperties()); 51 | $node->stmts[] = $this->buildConstructor(); 52 | $node->stmts[] = $this->buildJsonSerialize(); 53 | return $node; 54 | } 55 | 56 | return null; 57 | } 58 | 59 | public function buildJsonSerialize(): Node\Stmt\ClassMethod 60 | { 61 | $items = []; 62 | foreach ($this->columns as $column) { 63 | $items[] = new Node\Expr\ArrayItem( 64 | new Node\Expr\PropertyFetch( 65 | new Node\Expr\Variable('this'), 66 | new Node\Identifier(Str::camel($column->getName())), 67 | ), 68 | new Node\Scalar\String_($column->getName()) 69 | ); 70 | } 71 | 72 | return new Node\Stmt\ClassMethod(new Node\Identifier('jsonSerialize'), [ 73 | 'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC, 74 | 'returnType' => new Node\Identifier('mixed'), 75 | 'stmts' => [new Node\Stmt\Return_(new Node\Expr\Array_($items, [ 76 | 'kind' => Node\Expr\Array_::KIND_SHORT, 77 | ]))], 78 | ]); 79 | } 80 | 81 | public function buildConstructor(): Node\Stmt\ClassMethod 82 | { 83 | $method = new Node\Stmt\ClassMethod(new Node\Identifier('__construct'), [ 84 | 'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC, 85 | 'params' => [ 86 | new Node\Param( 87 | type: new Node\Identifier('\\' . $this->ref->getName()), 88 | var: new Node\Expr\Variable('model'), 89 | ), 90 | ], 91 | ]); 92 | 93 | foreach ($this->columns as $column) { 94 | $method->stmts[] = new Node\Stmt\Expression( 95 | new Node\Expr\Assign( 96 | new Node\Expr\PropertyFetch( 97 | new Node\Expr\Variable('this'), 98 | new Node\Identifier(Str::camel($column->getName())), 99 | ), 100 | new Node\Expr\PropertyFetch( 101 | new Node\Expr\Variable('model'), 102 | new Node\Identifier($column->getName()), 103 | ), 104 | ) 105 | ); 106 | } 107 | 108 | return $method; 109 | } 110 | 111 | public function buildProperties(): array 112 | { 113 | $result = []; 114 | /** @var Column $column */ 115 | foreach ($this->columns as $column) { 116 | $result[] = new Node\Stmt\Property( 117 | Node\Stmt\Class_::MODIFIER_PUBLIC, 118 | [ 119 | new Node\Stmt\PropertyProperty( 120 | new Node\VarLikeIdentifier(Str::camel($column->getName())) 121 | ), 122 | ], 123 | type: new Node\Identifier(name: $this->formatDatabaseType($column->getType(), true)), 124 | attrGroups: [ 125 | new Node\AttributeGroup([ 126 | new Node\Attribute(new Node\Name('Property'), [ 127 | new Node\Arg(value: new Node\Scalar\String_($column->getName()), name: new Node\Identifier('property')), 128 | new Node\Arg(value: new Node\Scalar\String_($column->getComment()), name: new Node\Identifier('title')), 129 | new Node\Arg(value: new Node\Scalar\String_($this->formatDatabaseType($column->getType())), name: new Node\Identifier('type')), 130 | ]), 131 | ]), 132 | ] 133 | ); 134 | } 135 | 136 | return $result; 137 | } 138 | 139 | protected function formatDatabaseType(string $type, bool $nullable = false): ?string 140 | { 141 | return match ($type) { 142 | 'tinyint', 'smallint', 'mediumint', 'int', 'bigint' => $nullable ? '?int' : 'int', 143 | 'bool', 'boolean' => $nullable ? '?bool' : 'bool', 144 | 'varchar', 'char' => $nullable ? '?string' : 'string', 145 | default => 'mixed', 146 | }; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Command/GenCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Generate swagger json file.'); 31 | } 32 | 33 | public function handle() 34 | { 35 | $config = $this->container->get(ConfigInterface::class); 36 | 37 | // are already generated in the listener if Swagger is enabled and automatically generated. 38 | if (! ($config->get('swagger.enable', false) && $config->get('swagger.auto_generate', false))) { 39 | $generator = $this->container->get(Generator::class); 40 | $generator->generate(); 41 | } 42 | 43 | $this->output->writeln('Generate swagger json success.'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Command/GenSchemaCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Generate swagger schemas.'); 37 | $this->addOption('name', 'N', InputOption::VALUE_OPTIONAL, 'The schema name.'); 38 | $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Whether to force generate the schema.'); 39 | $this->addOption('model', 'M', InputOption::VALUE_OPTIONAL, 'The model which used to generate schemas.'); 40 | } 41 | 42 | public function handle() 43 | { 44 | $name = $this->input->getOption('name'); 45 | $force = $this->input->getOption('force'); 46 | $model = $this->input->getOption('model'); 47 | 48 | if ($model) { 49 | if (! class_exists($model)) { 50 | $this->output->error(sprintf('The model %s is not exists.', $model)); 51 | return; 52 | } 53 | $ref = new ReflectionClass($model); 54 | /** @var Model $model */ 55 | $model = new $model(); 56 | } 57 | 58 | if (! $name) { 59 | if (! $model) { 60 | $this->output->error('The one of name or model must be exists.'); 61 | return; 62 | } 63 | 64 | $name = $ref->getShortName() . 'Schema'; 65 | } 66 | 67 | $dirname = BASE_PATH . '/app/Schema'; 68 | if (! is_dir($dirname)) { 69 | mkdir($dirname, 0755, true); 70 | } 71 | 72 | $path = $dirname . '/' . $name . '.php'; 73 | if (file_exists($path) && ! $force) { 74 | $this->output->error(sprintf('The path of schema %s is exists.', $path)); 75 | return; 76 | } 77 | 78 | $stub = file_get_contents(__DIR__ . '/stubs/schema.stub'); 79 | 80 | $code = str_replace('%NAME', $name, $stub); 81 | 82 | if (! $model) { 83 | file_put_contents($path, $code); 84 | return; 85 | } 86 | 87 | $lexer = new Emulative([ 88 | 'usedAttributes' => [ 89 | 'comments', 90 | 'startLine', 'endLine', 91 | 'startTokenPos', 'endTokenPos', 92 | ], 93 | ]); 94 | $parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7, $lexer); 95 | $printer = new Standard(); 96 | 97 | $traverser = new NodeTraverser(); 98 | 99 | $traverser->addVisitor(new ModelSchemaVisitor($ref, $model)); 100 | 101 | $stmts = $traverser->traverse($parser->parse($code)); 102 | 103 | file_put_contents($path, 'prettyPrint($stmts)); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Command/stubs/schema.stub: -------------------------------------------------------------------------------- 1 | id = $id; 20 | } 21 | 22 | public function jsonSerialize(): mixed 23 | { 24 | return [ 25 | 'id' => $this->id, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | [ 25 | GenCommand::class, 26 | GenSchemaCommand::class, 27 | ], 28 | 'listeners' => [ 29 | BootSwaggerListener::class, 30 | ], 31 | 'publish' => [ 32 | [ 33 | 'id' => 'config', 34 | 'description' => 'The config of swagger.', 35 | 'source' => __DIR__ . '/../publish/swagger.php', 36 | 'destination' => BASE_PATH . '/config/autoload/swagger.php', 37 | ], 38 | ], 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | config->get('swagger.scan.paths', null); 30 | if ($paths === null) { 31 | $paths = $this->config->get('annotations.scan.paths', []); 32 | } 33 | 34 | $userProcessors = $this->config->get('swagger.processors', []); 35 | if (! is_array($userProcessors)) { 36 | throw new InvalidArgumentException('The processors of swagger must be array.'); 37 | } 38 | 39 | $servers = (array) $this->config->get('swagger.server', []); 40 | 41 | $generator = new \OpenApi\Generator(); 42 | $openapi = $generator->setAliases(\OpenApi\Generator::DEFAULT_ALIASES) 43 | ->setNamespaces(\OpenApi\Generator::DEFAULT_NAMESPACES) 44 | ->setProcessors([ 45 | new Processors\DocBlockDescriptions(), 46 | new Processors\MergeIntoOpenApi(), 47 | new Processors\MergeIntoComponents(), 48 | new Processors\ExpandClasses(), 49 | new Processors\ExpandInterfaces(), 50 | new Processors\ExpandTraits(), 51 | new Processors\ExpandEnums(), 52 | new Processors\AugmentSchemas(), 53 | new Processors\AugmentProperties(), 54 | new BuildPathsProcessor(), 55 | new Processors\AugmentParameters(), 56 | new Processors\AugmentRefs(), 57 | new Processors\MergeJsonContent(), 58 | new Processors\MergeXmlContent(), 59 | new Processors\OperationId(), 60 | new Processors\CleanUnmerged(), 61 | ...$userProcessors, 62 | ]) 63 | ->generate($paths, validate: false); 64 | 65 | $jsonArray = Json::decode($openapi->toJson()); 66 | $paths = $jsonArray['paths'] ?? []; 67 | $jsonArray['paths'] = []; 68 | $result = []; 69 | foreach ($paths as $key => $path) { 70 | [$serverName, $key] = explode('|', $key, 2); 71 | if (empty($result[$serverName])) { 72 | $result[$serverName] = $jsonArray; 73 | } 74 | 75 | if ($svs = $servers[$serverName]['servers'] ?? null) { 76 | $result[$serverName]['servers'] = $svs; 77 | } 78 | 79 | if ($info = $servers[$serverName]['info'] ?? null) { 80 | $result[$serverName]['info'] = $info; 81 | } 82 | 83 | $result[$serverName]['paths'][$key] = $path; 84 | } 85 | 86 | $path = $this->config->get('swagger.json_dir', BASE_PATH . '/storage/swagger'); 87 | 88 | foreach ($result as $serverName => $json) { 89 | if (! is_dir($path)) { 90 | @mkdir($path, 0755, true); 91 | } 92 | file_put_contents(rtrim($path, '/') . '/' . $serverName . '.json', Json::encode($json)); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/HttpServer.php: -------------------------------------------------------------------------------- 1 | false, 32 | 'port' => 9500, 33 | 'json_dir' => BASE_PATH . '/storage/swagger', 34 | 'html' => null, 35 | 'url' => '/swagger', 36 | 'auto_generate' => false, 37 | 'scan' => [ 38 | 'paths' => null, 39 | ], 40 | ]; 41 | 42 | public function __construct(protected ContainerInterface $container, protected ResponseEmitter $emitter) 43 | { 44 | $this->config = array_merge($this->config, $this->container->get(ConfigInterface::class)->get('swagger', [])); 45 | } 46 | 47 | public function onRequest($request, $response): void 48 | { 49 | try { 50 | if ($request instanceof ServerRequestInterface) { 51 | $psr7Request = $request; 52 | } else { 53 | $psr7Request = Psr7Request::loadFromSwooleRequest($request); 54 | } 55 | 56 | $path = $psr7Request->getUri()->getPath(); 57 | if ($path === $this->config['url']) { 58 | $stream = new Stream($this->getHtml()); 59 | $contentType = 'text/html;charset=utf-8'; 60 | } else { 61 | $stream = new Stream($this->getMetadata($path)); 62 | $contentType = 'application/json;charset=utf-8'; 63 | } 64 | 65 | $psrResponse = (new Response())->setBody($stream)->setHeader('content-type', $contentType); 66 | 67 | $this->emitter->emit($psrResponse, $response); 68 | } catch (Throwable) { 69 | $this->emitter->emit( 70 | (new Response()) 71 | ->setBody(new Stream('Server Error')) 72 | ->setHeader('content-type', 'text/html;charset=utf-8'), 73 | $response 74 | ); 75 | } 76 | } 77 | 78 | protected function getMetadata(string $path): string 79 | { 80 | $path = rtrim($this->config['json_dir'], '/') . $path; 81 | $id = md5($path); 82 | if (isset($this->metadata[$id])) { 83 | return $this->metadata[$id]; 84 | } 85 | 86 | if (is_file($path) && file_exists($path)) { 87 | $metadata = file_get_contents($path); 88 | } else { 89 | $metadata = Json::encode([ 90 | 'openapi' => '3.0.0', 91 | ]); 92 | } 93 | 94 | return $this->metadata[$id] = $metadata; 95 | } 96 | 97 | protected function getHtml(): string 98 | { 99 | if (! empty($this->config['html'])) { 100 | return $this->config['html']; 101 | } 102 | 103 | return <<<'HTML' 104 | 105 | 106 | 107 | 108 | 109 | 113 | SwaggerUI 114 | 115 | 116 | 117 |
118 | 119 | 120 | 143 | 144 | 145 | HTML; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Listener/BootSwaggerListener.php: -------------------------------------------------------------------------------- 1 | container->get(ConfigInterface::class); 51 | if (! $config->get('swagger.enable', false)) { 52 | return; 53 | } 54 | 55 | $port = $config->get('swagger.port', 9500); 56 | 57 | // Setup SwaggerUI Server 58 | $servers = $config->get('server.servers'); 59 | foreach ($servers as $server) { 60 | if ($server['port'] == $port) { 61 | throw new InvalidArgumentException(sprintf('The swagger server port is invalid. Because it is conflicted with %s server.', $server['name'])); 62 | } 63 | } 64 | 65 | $servers[] = [ 66 | 'name' => 'swagger_' . uniqid(), 67 | 'type' => Server::SERVER_HTTP, 68 | 'host' => '0.0.0.0', 69 | 'port' => $port, 70 | 'sock_type' => SocketType::TCP, 71 | 'callbacks' => [ 72 | Event::ON_REQUEST => [HttpServer::class, 'onRequest'], 73 | ], 74 | ]; 75 | 76 | $config->set('server.servers', $servers); 77 | 78 | if ($config->get('swagger.auto_generate', false)) { 79 | $this->container->get(Generator::class)->generate(); 80 | } 81 | 82 | // Init Router 83 | $factory = $this->container->get(DispatcherFactory::class); 84 | $annotations = [ 85 | SA\Get::class, 86 | SA\Head::class, 87 | SA\Patch::class, 88 | SA\Post::class, 89 | SA\Put::class, 90 | SA\Delete::class, 91 | SA\Options::class, 92 | ]; 93 | 94 | foreach ($annotations as $annotation) { 95 | $methodCollector = AnnotationCollector::getMethodsByAnnotation($annotation); 96 | foreach ($methodCollector as $item) { 97 | $class = $item['class']; 98 | $method = $item['method']; 99 | /** @var MultipleAnnotation $annotation */ 100 | $annotation = $item['annotation']; 101 | 102 | $classAnnotations = AnnotationCollector::getClassAnnotations($class); 103 | $methodAnnotations = AnnotationCollector::getClassMethodAnnotation($class, $method); 104 | 105 | $serverAnnotations = Util::findAnnotations($methodAnnotations, SA\HyperfServer::class); 106 | if (! $serverAnnotations) { 107 | $serverAnnotations = Util::findAnnotations($classAnnotations, SA\HyperfServer::class); 108 | } 109 | 110 | $middlewareAnnotations = Util::findAnnotations($methodAnnotations, Middleware::class); 111 | $middlewareAnnotations = array_merge($middlewareAnnotations, Util::findAnnotations($classAnnotations, Middleware::class)); 112 | 113 | /** @var Operation $opera */ 114 | foreach ($annotation->toAnnotations() as $opera) { 115 | /** @var SA\HyperfServer $serverAnnotation */ 116 | foreach ($serverAnnotations as $serverAnnotation) { 117 | $factory->getRouter($serverAnnotation->name)->addRoute( 118 | [$opera->method], 119 | $opera->path, 120 | [$class, $method], 121 | [ 122 | 'middleware' => value(static function () use ($middlewareAnnotations) { 123 | $result = []; 124 | /** @var Middleware $annotation */ 125 | foreach ($middlewareAnnotations as $annotation) { 126 | $result[] = $annotation->middleware; 127 | } 128 | 129 | return $result; 130 | }), 131 | ] 132 | ); 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Processor/BuildPathsProcessor.php: -------------------------------------------------------------------------------- 1 | openapi->paths)) { 30 | foreach ($analysis->openapi->paths as $annotation) { 31 | if (empty($annotation->path)) { 32 | $annotation->_context->logger->warning($annotation->identity() . ' is missing required property "path" in ' . $annotation->_context); 33 | } elseif (isset($paths[$annotation->path])) { 34 | $paths[$annotation->path]->mergeProperties($annotation); 35 | $analysis->annotations->detach($annotation); 36 | } else { 37 | $paths[$annotation->path] = $annotation; 38 | } 39 | } 40 | } 41 | 42 | /** @var OA\Operation[] $operations */ 43 | $operations = $analysis->unmerged()->getAnnotationsOfType(OA\Operation::class); 44 | 45 | // Merge @OA\Operations into existing @OA\PathItems or create a new one. 46 | foreach ($operations as $operation) { 47 | $class = $operation->_context->namespace . '\\' . $operation->_context->class; 48 | /** @var null|HyperfServer $serverAnnotation */ 49 | $serverAnnotation = AnnotationCollector::getClassAnnotation($class, HyperfServer::class); 50 | if (! $serverAnnotation) { 51 | continue; 52 | } 53 | 54 | $path = $serverAnnotation->name . '|' . $operation->path; 55 | 56 | if (empty($paths[$path])) { 57 | $paths[$path] = $pathItem = new OA\PathItem( 58 | [ 59 | 'path' => $path, 60 | '_context' => new Context(['generated' => true], $operation->_context), 61 | ] 62 | ); 63 | $analysis->addAnnotation($pathItem, $pathItem->_context); 64 | } 65 | if ($paths[$path]->merge([$operation])) { 66 | $operation->_context->logger->warning('Unable to merge ' . $operation->identity() . ' in ' . $operation->_context); 67 | } 68 | } 69 | if ($paths) { 70 | $analysis->openapi->paths = array_values($paths); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Request/RuleCollector.php: -------------------------------------------------------------------------------- 1 | getCallbackByContext(); 36 | 37 | return ValidationCollector::get($callback[0], $callback[1], 'rules'); 38 | } 39 | 40 | public function attributes(): array 41 | { 42 | $callback = $this->getCallbackByContext(); 43 | 44 | return ValidationCollector::get($callback[0], $callback[1], 'attribute'); 45 | } 46 | 47 | protected function getCallbackByContext(): array 48 | { 49 | /** @var null|Dispatched $dispatched */ 50 | $dispatched = RequestContext::getOrNull()?->getAttribute(Dispatched::class); 51 | if (! $dispatched) { 52 | throw new RuntimeException('The request is invalid.'); 53 | } 54 | 55 | $callback = $dispatched->handler?->callback; 56 | if (! $callback) { 57 | throw new RuntimeException('The SwaggerRequest is only used by swagger annotations.'); 58 | } 59 | 60 | return $this->prepareHandler($callback); 61 | } 62 | 63 | /** 64 | * @see \Hyperf\HttpServer\CoreMiddleware::prepareHandler() 65 | */ 66 | protected function prepareHandler(array|string $handler): array 67 | { 68 | if (is_string($handler)) { 69 | if (str_contains($handler, '@')) { 70 | return explode('@', $handler); 71 | } 72 | $array = explode('::', $handler); 73 | if (! isset($array[1]) && class_exists($handler) && method_exists($handler, '__invoke')) { 74 | $array[1] = '__invoke'; 75 | } 76 | return [$array[0], $array[1] ?? null]; 77 | } 78 | if (is_array($handler) && isset($handler[0], $handler[1])) { 79 | return $handler; 80 | } 81 | throw new RuntimeException('Handler not exist.'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Request/ValidationCollector.php: -------------------------------------------------------------------------------- 1 | {$field}) && $property->{$field}) { 53 | $data[$property->name] = $property->{$field}; 54 | } 55 | } 56 | return $data; 57 | } 58 | 59 | protected static function collectJsonContentRequestBody($methodAnnotations, array $data, string $field): array 60 | { 61 | /** @var null|RequestBody $body */ 62 | $body = Util::findAnnotations($methodAnnotations, RequestBody::class)[0] ?? null; 63 | if (! $body) { 64 | return $data; 65 | } 66 | 67 | if (! $body->_content instanceof JsonContent) { 68 | return $data; 69 | } 70 | 71 | if (! is_array($body->_content->properties)) { 72 | return $data; 73 | } 74 | 75 | foreach ($body->_content->properties as $property) { 76 | if ($property instanceof Property) { 77 | if (isset($property->{$field}) && $property->{$field}) { 78 | $data[$property->property] = $property->{$field}; 79 | } 80 | } 81 | } 82 | 83 | return $data; 84 | } 85 | 86 | protected static function collectMediaTypeRequestBody($methodAnnotations, array $data, string $field): array 87 | { 88 | /** @var null|RequestBody $body */ 89 | $body = Util::findAnnotations($methodAnnotations, RequestBody::class)[0] ?? null; 90 | if (! $body || ! is_array($body->content)) { 91 | return $data; 92 | } 93 | 94 | foreach ($body->content as $content) { 95 | if (! $content instanceof MediaType) { 96 | continue; 97 | } 98 | 99 | /* @phpstan-ignore-next-line */ 100 | if (! $content->schema || ! is_array($content->schema->properties)) { 101 | continue; 102 | } 103 | foreach ($content->schema->properties as $property) { 104 | if (! $property instanceof Property) { 105 | continue; 106 | } 107 | if (isset($property->{$field}) && $property->{$field}) { 108 | $data[$property->property] = $property->{$field}; 109 | } 110 | } 111 | } 112 | return $data; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Util.php: -------------------------------------------------------------------------------- 1 | className() === $class) { 29 | $result = array_merge($result, $annotation->toAnnotations()); 30 | } 31 | } 32 | } 33 | 34 | return $result; 35 | } 36 | } 37 | --------------------------------------------------------------------------------