├── .gitattributes ├── .gitignore ├── src ├── web │ ├── favicon.png │ ├── rapidoc.html │ ├── scalar.html │ ├── redoc.html │ └── swagger.html ├── Exception │ └── ApiDocsException.php ├── Annotation │ ├── ApiFormData.php │ ├── ApiHeader.php │ ├── ApiVariable.php │ ├── ApiModel.php │ ├── Api.php │ ├── ApiSecurity.php │ ├── ApiOperation.php │ ├── BaseParam.php │ ├── ApiModelProperty.php │ └── ApiResponse.php ├── DTO │ └── GlobalResponse.php ├── ConfigProvider.php ├── Listener │ ├── AfterWorkerStartListener.php │ ├── AfterDtoStartListener.php │ └── BootAppRouteListener.php ├── Ast │ └── ResponseVisitor.php └── Swagger │ ├── SwaggerConfig.php │ ├── SwaggerController.php │ ├── SwaggerCommon.php │ ├── SwaggerUiController.php │ ├── SwaggerOpenApi.php │ ├── GenerateProxyClass.php │ ├── SwaggerComponents.php │ ├── GenerateResponses.php │ ├── SwaggerPaths.php │ └── GenerateParameters.php ├── phpunit.xml ├── LICENSE ├── composer.json ├── .php-cs-fixer.php ├── publish └── api_docs.php ├── README2.md ├── README.md └── README_EN.md /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | /example export-ignore 3 | /.github export-ignore 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | *.cache 4 | *.log 5 | .DS_Store 6 | .idea 7 | 8 | -------------------------------------------------------------------------------- /src/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw2066/api-docs/HEAD/src/web/favicon.png -------------------------------------------------------------------------------- /src/Exception/ApiDocsException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /src/Annotation/ApiVariable.php: -------------------------------------------------------------------------------- 1 | data = $data; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Annotation/Api.php: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | ./tests/ 14 | 15 | -------------------------------------------------------------------------------- /src/Annotation/ApiOperation.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scalar API Reference 5 | 6 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 26 | 27 | -------------------------------------------------------------------------------- /src/web/redoc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redoc 5 | 6 | 7 | 8 | 9 | 10 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Annotation/BaseParam.php: -------------------------------------------------------------------------------- 1 | in; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Annotation/ApiModelProperty.php: -------------------------------------------------------------------------------- 1 | phpType = $simpleType->getValue(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Tang 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 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | [ 17 | ], 18 | 'listeners' => [ 19 | AfterDtoStartListener::class, 20 | BootAppRouteListener::class, 21 | AfterWorkerStartListener::class, 22 | ], 23 | 'annotations' => [ 24 | 'scan' => [ 25 | 'paths' => [ 26 | __DIR__, 27 | ], 28 | ], 29 | ], 30 | 'publish' => [ 31 | [ 32 | 'id' => 'config', 33 | 'description' => 'The config for api-docs.', 34 | 'source' => __DIR__ . '/../publish/api_docs.php', 35 | 'destination' => BASE_PATH . '/config/autoload/api_docs.php', 36 | ], 37 | ], 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Listener/AfterWorkerStartListener.php: -------------------------------------------------------------------------------- 1 | workerId === 0) { 38 | BootAppRouteListener::$massage && $this->logger->info(BootAppRouteListener::$massage); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tangwei/apidocs", 3 | "description": "A swagger library for Hyperf.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "php", 7 | "docs", 8 | "hyperf", 9 | "swagger", 10 | "hyperf swagger" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "TangWei", 15 | "email": "tw2066@163.com", 16 | "homepage": "https://github.com/tw2066", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=8.1", 22 | "tangwei/dto": "~3.1.0", 23 | "zircote/swagger-php": "^4.8||^5.1" 24 | }, 25 | "require-dev": { 26 | "friendsofphp/php-cs-fixer": "^3.0", 27 | "mockery/mockery": "^1.0", 28 | "phpstan/phpstan": "^1.0", 29 | "phpunit/phpunit": ">=7.0", 30 | "symfony/var-dumper": "^5.1" 31 | }, 32 | "suggest": { 33 | "tangwei/knife4j-ui": "It can be as small as a dagger, lightweight, and powerful!" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Hyperf\\ApiDocs\\": "src/" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "HyperfTest\\ApiDocs\\": "tests/" 43 | } 44 | }, 45 | "extra": { 46 | "hyperf": { 47 | "config": "Hyperf\\ApiDocs\\ConfigProvider" 48 | }, 49 | "branch-alias": { 50 | "dev-master": "3.1.x-dev" 51 | } 52 | }, 53 | "config": { 54 | "optimize-autoloader": true, 55 | "sort-packages": true 56 | }, 57 | "scripts": { 58 | "test": "phpunit -c phpunit.xml --colors=always", 59 | "analyse": "phpstan analyse --memory-limit 1024M -l 0 ./src", 60 | "cs-fix": "php-cs-fixer fix src && php-cs-fixer fix tests" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/web/swagger.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/Annotation/ApiResponse.php: -------------------------------------------------------------------------------- 1 | setReturnType($returnType); 23 | } 24 | 25 | protected function setReturnType($returnType): void 26 | { 27 | if ($returnType instanceof PhpType) { 28 | $this->returnType = $returnType->getValue(); 29 | return; 30 | } 31 | if (is_object($returnType)) { 32 | $this->returnType = $returnType; 33 | return; 34 | } 35 | if (is_string($returnType) && class_exists($returnType)) { 36 | $this->returnType = $returnType; 37 | return; 38 | } 39 | // eg: [class] 40 | if (is_array($returnType) && count($returnType) > 0) { 41 | if ($returnType[0] instanceof PhpType) { 42 | $this->returnType = [$returnType[0]->getValue()]; 43 | return; 44 | } 45 | if (is_string($returnType[0]) && class_exists($returnType[0])) { 46 | $this->returnType = $returnType; 47 | return; 48 | } 49 | if (is_object($returnType[0])) { 50 | $this->returnType = $returnType; 51 | return; 52 | } 53 | } 54 | // 空数组 55 | if (is_array($returnType) && count($returnType) == 0) { 56 | $this->returnType = 'array'; 57 | return; 58 | } 59 | 60 | if ($returnType === null) { 61 | $this->returnType = null; 62 | return; 63 | } 64 | 65 | throw new ApiDocsException('ApiResponse: Unsupported data type'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 8 | ->setRules([ 9 | '@PSR2' => true, 10 | '@Symfony' => true, 11 | '@DoctrineAnnotation' => true, 12 | '@PhpCsFixer' => true, 13 | 'header_comment' => [ 14 | 'comment_type' => 'PHPDoc', 15 | 'header' => $header, 16 | 'separate' => 'none', 17 | 'location' => 'after_declare_strict', 18 | ], 19 | 'array_syntax' => [ 20 | 'syntax' => 'short' 21 | ], 22 | 'list_syntax' => [ 23 | 'syntax' => 'short' 24 | ], 25 | 'concat_space' => [ 26 | 'spacing' => 'one' 27 | ], 28 | 'blank_line_before_statement' => [ 29 | 'statements' => [ 30 | 'declare', 31 | ], 32 | ], 33 | 'general_phpdoc_annotation_remove' => [ 34 | 'annotations' => [ 35 | 'author' 36 | ], 37 | ], 38 | 'ordered_imports' => [ 39 | 'imports_order' => [ 40 | 'class', 'function', 'const', 41 | ], 42 | 'sort_algorithm' => 'alpha', 43 | ], 44 | 'single_line_comment_style' => [ 45 | 'comment_types' => [ 46 | ], 47 | ], 48 | 'yoda_style' => [ 49 | 'always_move_variable' => false, 50 | 'equal' => false, 51 | 'identical' => false, 52 | ], 53 | 'phpdoc_align' => [ 54 | 'align' => 'left', 55 | ], 56 | 'multiline_whitespace_before_semicolons' => [ 57 | 'strategy' => 'no_multi_line', 58 | ], 59 | 'constant_case' => [ 60 | 'case' => 'lower', 61 | ], 62 | 'class_attributes_separation' => true, 63 | 'combine_consecutive_unsets' => true, 64 | 'declare_strict_types' => true, 65 | 'linebreak_after_opening_tag' => true, 66 | 'lowercase_static_reference' => true, 67 | 'no_useless_else' => true, 68 | 'no_unused_imports' => true, 69 | 'not_operator_with_successor_space' => true, 70 | 'not_operator_with_space' => false, 71 | 'ordered_class_elements' => true, 72 | 'php_unit_strict' => false, 73 | 'phpdoc_separation' => false, 74 | 'single_quote' => true, 75 | 'standardize_not_equals' => true, 76 | 'multiline_comment_opening_closing' => true, 77 | ]) 78 | ->setFinder( 79 | PhpCsFixer\Finder::create() 80 | ->exclude('vendor') 81 | ->in(__DIR__) 82 | ) 83 | ->setUsingCache(false); 84 | -------------------------------------------------------------------------------- /src/Ast/ResponseVisitor.php: -------------------------------------------------------------------------------- 1 | factory = new BuilderFactory(); 25 | } 26 | 27 | public function leaveNode(Node $node) 28 | { 29 | if ($node instanceof Node\Stmt\Property) { 30 | $propertyName = $node->props[0]->name->name; 31 | // 存在可变变量 32 | if (isset($this->propertyArr[$propertyName])) { 33 | $propertyTypeName = $this->propertyArr[$propertyName]; 34 | if (is_array($propertyTypeName)) { 35 | $arrayType = $propertyTypeName[0]; 36 | $name = new Node\Name('\Hyperf\DTO\Annotation\ArrayType'); 37 | $node->attrGroups[] = new Node\AttributeGroup([ 38 | new Node\Attribute( 39 | $name, 40 | $this->buildAttributeArgs(new ArrayType($arrayType)), 41 | ), 42 | ]); 43 | 44 | $propertyTypeName = 'array'; 45 | } 46 | $node->type = new Node\Identifier($propertyTypeName); 47 | // $node->props[0]->default = null; 48 | } 49 | } 50 | if ($node instanceof Node\Stmt\Class_) { 51 | $node->name = $this->generateClassName; 52 | } 53 | if ($node instanceof Node\Stmt\Namespace_) { 54 | $name = new Node\Name('ApiDocs\\Proxy'); 55 | $node->name = $name; 56 | } 57 | } 58 | 59 | protected function buildAttributeArgs(AbstractAnnotation $annotation, array $args = []): array 60 | { 61 | return $this->factory->args(array_merge($args, $this->getNotDefaultPropertyFromAnnotation($annotation))); 62 | } 63 | 64 | protected function getNotDefaultPropertyFromAnnotation(AbstractAnnotation $annotation): array 65 | { 66 | $properties = []; 67 | $ref = new ReflectionClass($annotation); 68 | foreach ($ref->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { 69 | if ($property->hasDefaultValue() && $property->getDefaultValue() === $property->getValue($annotation)) { 70 | continue; 71 | } 72 | $properties[$property->getName()] = $property->getValue($annotation); 73 | } 74 | return $properties; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Swagger/SwaggerConfig.php: -------------------------------------------------------------------------------- 1 | get('api_docs', []); 37 | $jsonMapper = Mapper::getJsonMapper('bIgnoreVisibility'); 38 | // 私有属性和函数 39 | $jsonMapper->bIgnoreVisibility = true; 40 | $jsonMapper->map($data, $this); 41 | } 42 | 43 | public function setPrefixUrl(string $prefix_url): void 44 | { 45 | $this->prefix_url = '/' . trim($prefix_url, '/'); 46 | } 47 | 48 | public function isEnable(): bool 49 | { 50 | return $this->enable; 51 | } 52 | 53 | public function getOutputDir(): string 54 | { 55 | return $this->output_dir; 56 | } 57 | 58 | public function getProxyDir(): string 59 | { 60 | return $this->proxy_dir ?: BASE_PATH . '/runtime/container/proxy/'; 61 | } 62 | 63 | public function setProxyDir(string $proxy_dir): void 64 | { 65 | $this->proxy_dir = rtrim($proxy_dir, '/') . '/'; 66 | } 67 | 68 | public function getPrefixUrl(): string 69 | { 70 | return $this->prefix_url ?: 'swagger'; 71 | } 72 | 73 | public function isValidationCustomAttributes(): bool 74 | { 75 | return $this->validation_custom_attributes; 76 | } 77 | 78 | public function getResponses(): array 79 | { 80 | return $this->responses; 81 | } 82 | 83 | /** 84 | * @return array [ 85 | * 'info' => [], 86 | * 'servers' => [], 87 | * 'externalDocs' => [], 88 | * 'components' => [ 89 | * 'securitySchemes'=>[] 90 | * ], 91 | * 'openapi'=>'', 92 | * 'security'=>[], 93 | * ] 94 | */ 95 | public function getSwagger(): array 96 | { 97 | return $this->swagger; 98 | } 99 | 100 | public function getResponsesCode(): string 101 | { 102 | return $this->responses_code; 103 | } 104 | 105 | public function getFormat(): string 106 | { 107 | return $this->format == 'json' ? 'json' : 'yaml'; 108 | } 109 | 110 | public function getPrefixSwaggerResources(): string 111 | { 112 | return $this->prefix_swagger_resources; 113 | } 114 | 115 | public function getGlobalReturnResponsesClass(): string 116 | { 117 | return $this->global_return_responses_class; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Listener/AfterDtoStartListener.php: -------------------------------------------------------------------------------- 1 | serverConfig; 45 | $router = $event->router; 46 | 47 | if (! $this->swaggerConfig->isEnable()) { 48 | return; 49 | } 50 | if (! $this->swaggerConfig->getOutputDir()) { 51 | return; 52 | } 53 | 54 | $this->swaggerOpenApi->init($server['name']); 55 | 56 | if ($this->dtoConfig->isScanCacheable()) { 57 | return; 58 | } 59 | 60 | /** @var SwaggerPaths $swagger */ 61 | $swagger = make(SwaggerPaths::class, [$server['name']]); 62 | foreach ($router->getData() ?? [] as $routeData) { 63 | foreach ($routeData ?? [] as $methods => $handlerArr) { 64 | array_walk_recursive($handlerArr, function ($item) use ($swagger, $methods) { 65 | if ($item instanceof Handler && ! ($item->callback instanceof Closure)) { 66 | $prepareHandler = $this->prepareHandler($item->callback); 67 | if (count($prepareHandler) > 1) { 68 | [$controller, $methodName] = $prepareHandler; 69 | $swagger->addPath($controller, $methodName, $item->route, $methods); 70 | } 71 | } 72 | }); 73 | } 74 | } 75 | 76 | $schemas = $this->swaggerComponents->getSchemas(); 77 | $this->swaggerOpenApi->setComponentsSchemas($schemas); 78 | $this->swaggerOpenApi->save($server['name']); 79 | 80 | $this->swaggerOpenApi->clean(); 81 | $this->swaggerComponents->setSchemas([]); 82 | 83 | $this->logger->debug('swagger server:[' . $server['name'] . '] file has been generated'); 84 | } 85 | 86 | protected function prepareHandler($handler): array 87 | { 88 | if (is_string($handler)) { 89 | if (str_contains($handler, '@')) { 90 | return explode('@', $handler); 91 | } 92 | return explode('::', $handler); 93 | } 94 | if (is_array($handler) && isset($handler[0], $handler[1])) { 95 | return $handler; 96 | } 97 | throw new RuntimeException('Handler not exist.'); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Swagger/SwaggerController.php: -------------------------------------------------------------------------------- 1 | outputDir = $this->swaggerConfig->getOutputDir(); 33 | $this->uiFileList = is_dir($this->swaggerUiPath) ? scandir($this->swaggerUiPath) : []; 34 | $this->swaggerFileList = scandir($this->outputDir); 35 | } 36 | 37 | public function getFile(string $file): PsrResponseInterface 38 | { 39 | if (!in_array($file, $this->uiFileList)) { 40 | throw new ApiDocsException('File does not exist'); 41 | } 42 | $file = $this->swaggerUiPath . '/' . $file; 43 | return $this->fileResponse($file); 44 | } 45 | 46 | public function getJsonFile(string $httpName): PsrResponseInterface 47 | { 48 | $file = $httpName . '.json'; 49 | if (!in_array($file, $this->swaggerFileList)) { 50 | throw new ApiDocsException('File does not exist'); 51 | } 52 | $filePath = $this->outputDir . '/' . $file; 53 | return $this->fileResponse($filePath); 54 | } 55 | 56 | public function getYamlFile(string $httpName): PsrResponseInterface 57 | { 58 | $file = $httpName . '.yaml'; 59 | if (!in_array($file, $this->swaggerFileList)) { 60 | throw new ApiDocsException('File does not exist'); 61 | } 62 | $filePath = $this->outputDir . '/' . $file; 63 | return $this->fileResponse($filePath); 64 | } 65 | 66 | protected function fileResponse(string $filePath) 67 | { 68 | if (!$this->pharRunning() && Constant::ENGINE == 'Swoole') { // phar报错 69 | $stream = new SwooleFileStream($filePath); 70 | } elseif (Constant::ENGINE == 'Swow') { 71 | /* @phpstan-ignore-next-line */ 72 | $stream = new BufferStream(file_get_contents($filePath)); 73 | } else { 74 | $stream = new SwooleStream(file_get_contents($filePath)); 75 | } 76 | $response = $this->response->withBody($stream); 77 | 78 | $pathinfo = pathinfo($filePath); 79 | switch ($pathinfo['extension']) { 80 | case 'js': 81 | case 'map': 82 | $response = $response->withAddedHeader('content-type', 'application/javascript')->withAddedHeader('cache-control', 'max-age=43200'); 83 | break; 84 | case 'css': 85 | $response = $response->withAddedHeader('content-type', 'text/css')->withAddedHeader('cache-control', 'max-age=43200'); 86 | break; 87 | } 88 | return $response; 89 | } 90 | 91 | protected function getSwaggerFileUrl($serverName): string 92 | { 93 | return $this->swaggerConfig->getPrefixUrl() . '/' . $serverName . '.' . $this->swaggerConfig->getFormat(); 94 | } 95 | 96 | private function pharRunning(): bool 97 | { 98 | return class_exists('Phar') && Phar::running(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Listener/BootAppRouteListener.php: -------------------------------------------------------------------------------- 1 | swaggerConfig->isValidationCustomAttributes()) { 47 | DtoValidation::$isValidationCustomAttributes = true; 48 | } 49 | 50 | if (! $this->swaggerConfig->isEnable()) { 51 | $this->logger->info('api_docs swagger not enable'); 52 | return; 53 | } 54 | if (! $this->swaggerConfig->getOutputDir()) { 55 | $this->logger->error('/config/autoload/api_docs.php need set output_dir'); 56 | return; 57 | } 58 | $prefix = $this->swaggerConfig->getPrefixUrl(); 59 | $servers = $this->config->get('server.servers'); 60 | $httpServerRouter = null; 61 | $httpServer = null; 62 | foreach ($servers as $server) { 63 | $router = $this->dispatcherFactory->getRouter($server['name']); 64 | if (empty($httpServerRouter) && $server['type'] == Server::SERVER_HTTP) { 65 | $httpServerRouter = $router; 66 | $httpServer = $server; 67 | } 68 | } 69 | if (empty($httpServerRouter)) { 70 | $this->logger->warning('Swagger: http Service not started'); 71 | return; 72 | } 73 | // 添加路由 74 | $httpServerRouter->addGroup($prefix, function ($route) { 75 | $route->get('', [SwaggerUiController::class, 'swagger']); 76 | $route->get('/redoc', [SwaggerUiController::class, 'redoc']); 77 | $route->get('/rapidoc', [SwaggerUiController::class, 'rapidoc']); 78 | $route->get('/scalar', [SwaggerUiController::class, 'scalar']); 79 | $route->get('/doc', [SwaggerUiController::class, 'knife4j']); 80 | $route->get('/swagger-resources', [SwaggerUiController::class, 'swaggerResources']); 81 | $route->get('/v3/api-docs/swagger-config', [SwaggerUiController::class, 'swaggerConfig']); 82 | $route->get('/webjars/{file:.*}', [SwaggerUiController::class, 'knife4jFile']); 83 | $route->get('/favicon.ico', [SwaggerUiController::class, 'favicon']); 84 | 85 | $route->get('/{httpName}.json', [SwaggerController::class, 'getJsonFile']); 86 | $route->get('/{httpName}.yaml', [SwaggerController::class, 'getYamlFile']); 87 | $route->get('/{file}', [SwaggerController::class, 'getFile']); 88 | }); 89 | self::$httpServerName = $httpServer['name']; 90 | $isKnife4j = Composer::hasPackage('tangwei/knife4j-ui'); 91 | $docHtml = $isKnife4j ? '/doc' : ''; 92 | 93 | $host = $httpServer['host'] == '0.0.0.0' ? '127.0.0.1' : $httpServer['host']; 94 | static::$massage = 'Swagger docs url at http://' . $host . ':' . $httpServer['port'] . $prefix . $docHtml; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Swagger/SwaggerCommon.php: -------------------------------------------------------------------------------- 1 | getSimpleClassName($className); 22 | } 23 | 24 | public function simpleClassNameClear(): void 25 | { 26 | static::$className = []; 27 | static::$simpleClassName = []; 28 | } 29 | 30 | /** 31 | * 获取简单php类名. 32 | */ 33 | public function getSimpleClassName(?string $className): string 34 | { 35 | if ($className === null) { 36 | $className = 'Null'; 37 | } 38 | $className = ltrim($className, '\\'); 39 | if (isset(self::$className[$className])) { 40 | return self::$className[$className]; 41 | } 42 | $pos = strrpos($className, '\\'); 43 | $simpleClassName = $className; 44 | if ($pos !== false) { 45 | $simpleClassName = substr($className, $pos + 1); 46 | } 47 | 48 | $simpleClassName = $this->getSimpleClassNameNum(ucfirst($simpleClassName)); 49 | self::$className[$className] = $simpleClassName; 50 | return $simpleClassName; 51 | } 52 | 53 | /** 54 | * 获取swagger类型. 55 | */ 56 | public function getSwaggerType(mixed $phpType): string 57 | { 58 | return match ($phpType) { 59 | 'int', 'integer' => 'integer', 60 | 'boolean', 'bool' => 'boolean', 61 | 'double', 'float', 'number' => 'number', 62 | 'array' => 'array', 63 | 'object' => 'object', 64 | 'string' => 'string', 65 | default => 'null', 66 | }; 67 | } 68 | 69 | /** 70 | * 通过PHP类型 获取SwaggerType类型. 71 | */ 72 | public function getSimpleType2SwaggerType(?string $phpType): ?string 73 | { 74 | return match ($phpType) { 75 | 'int', 'integer' => 'integer', 76 | 'boolean', 'bool' => 'boolean', 77 | 'double', 'float' => 'number', 78 | 'string', 'mixed' => 'string', 79 | default => null, 80 | }; 81 | } 82 | 83 | public function getPhpType(mixed $type): string 84 | { 85 | if (is_string($type) && $this->isSimpleType($type)) { 86 | return $type; 87 | } 88 | if ($type instanceof PhpType) { 89 | return $type->getValue(); 90 | } 91 | 92 | if (is_object($type) && $type::class != 'stdClass') { 93 | return '\\' . $type::class; 94 | } 95 | if (is_string($type) && class_exists($type)) { 96 | return '\\' . trim($type, '\\'); 97 | } 98 | return 'mixed'; 99 | } 100 | 101 | public function getPropertyDefaultValue(string $className, ReflectionProperty $reflectionProperty) 102 | { 103 | $default = Generator::UNDEFINED; 104 | try { 105 | $obj = \Hyperf\Support\make($className); 106 | if ($reflectionProperty->isInitialized($obj)) { 107 | $default = $reflectionProperty->getValue($obj); 108 | } 109 | } catch (Throwable) { 110 | $fieldName = $reflectionProperty->getName(); 111 | $classVars = get_class_vars($className); 112 | // 别名会获取不到默认值 113 | if (isset($classVars[$fieldName])) { 114 | $default = $classVars[$fieldName]; 115 | } 116 | } 117 | return $default; 118 | } 119 | 120 | private function getSimpleClassNameNum(string $className, $num = 0): string 121 | { 122 | $simpleClassName = $className . ($num > 0 ? '_' . $num : ''); 123 | if (isset(self::$simpleClassName[$simpleClassName])) { 124 | return $this->getSimpleClassNameNum($className, $num + 1); 125 | } 126 | self::$simpleClassName[$simpleClassName] = $num; 127 | return $simpleClassName; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Swagger/SwaggerUiController.php: -------------------------------------------------------------------------------- 1 | docsWebPath . '/swagger.html'; 20 | $contents = file_get_contents($filePath); 21 | $contents = str_replace('{{$prefixUrl}}', $this->swaggerConfig->getPrefixUrl(), $contents); 22 | $contents = str_replace('{{$path}}', $this->swaggerConfig->getPrefixSwaggerResources(), $contents); 23 | // $contents = str_replace('{{$url}}', $this->getSwaggerFileUrl(BootAppRouteListener::$httpServerName), $contents); 24 | $serverNameAll = array_reverse($this->swaggerOpenApi->serverNameAll); 25 | $urls = ''; 26 | foreach ($serverNameAll as $serverName) { 27 | $url = $this->getSwaggerFileUrl($serverName); 28 | $urls .= "{url: '{$url}', name: '{$serverName} server'},"; 29 | } 30 | $contents = str_replace('"{{$urls}}"', $urls, $contents); 31 | return $this->response->withAddedHeader('content-type', 'text/html')->withBody(new SwooleStream($contents)); 32 | } 33 | 34 | public function redoc(): PsrResponseInterface 35 | { 36 | $filePath = $this->docsWebPath . '/redoc.html'; 37 | $contents = file_get_contents($filePath); 38 | $contents = str_replace('{{$url}}', $this->getSwaggerFileUrl(BootAppRouteListener::$httpServerName), $contents); 39 | return $this->response->withAddedHeader('content-type', 'text/html')->withBody(new SwooleStream($contents)); 40 | } 41 | 42 | public function rapidoc(): PsrResponseInterface 43 | { 44 | // https://rapidocweb.com/examples.html 45 | $filePath = $this->docsWebPath . '/rapidoc.html'; 46 | $contents = file_get_contents($filePath); 47 | $contents = str_replace('{{$url}}', BootAppRouteListener::$httpServerName . '.' . $this->swaggerConfig->getFormat(), $contents); 48 | return $this->response->withAddedHeader('content-type', 'text/html')->withBody(new SwooleStream($contents)); 49 | } 50 | 51 | public function scalar(): PsrResponseInterface 52 | { 53 | // https://github.com/scalar/scalar 54 | $filePath = $this->docsWebPath . '/scalar.html'; 55 | $contents = file_get_contents($filePath); 56 | $contents = str_replace('{{$url}}', BootAppRouteListener::$httpServerName . '.' . $this->swaggerConfig->getFormat(), $contents); 57 | return $this->response->withAddedHeader('content-type', 'text/html')->withBody(new SwooleStream($contents)); 58 | } 59 | 60 | public function knife4j() 61 | { 62 | $filePath = $this->swaggerUiPath . '/doc.html'; 63 | $contents = file_get_contents($filePath); 64 | return $this->response->withAddedHeader('content-type', 'text/html')->withBody(new SwooleStream($contents)); 65 | } 66 | 67 | public function swaggerResources() 68 | { 69 | $serverNameAll = array_reverse($this->swaggerOpenApi->serverNameAll); 70 | $urls = []; 71 | foreach ($serverNameAll as $serverName) { 72 | $urls[] = [ 73 | 'name' => "{$serverName} server", 74 | 'url' => $serverName . '.' . $this->swaggerConfig->getFormat(), 75 | ]; 76 | } 77 | 78 | return $urls; 79 | } 80 | 81 | /** 82 | * 适配knife4j 4.5.0版本. 83 | * https://gitee.com/xiaoym/knife4j/issues/I986E2 84 | */ 85 | public function swaggerConfig(): array 86 | { 87 | $urls = $this->swaggerResources(); 88 | $data['urls'] = $urls; 89 | return $data; 90 | } 91 | 92 | public function knife4jFile(string $file): PsrResponseInterface 93 | { 94 | $file = str_replace('..', '', $file); 95 | $file = '/webjars/' . $file; 96 | $file = $this->swaggerUiPath . '/' . $file; 97 | return $this->fileResponse($file); 98 | } 99 | 100 | public function favicon(): PsrResponseInterface 101 | { 102 | $file = $this->docsWebPath . '/favicon.png'; 103 | return $this->fileResponse($file); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /publish/api_docs.php: -------------------------------------------------------------------------------- 1 | env('APP_ENV') !== 'prod', 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | 生成swagger文件格式 22 | |-------------------------------------------------------------------------- 23 | | 24 | | 支持json和yaml 25 | | 26 | */ 27 | 'format' => 'json', 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | 生成swagger文件路径 32 | |-------------------------------------------------------------------------- 33 | */ 34 | 'output_dir' => BASE_PATH . '/runtime/container', 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | 生成代理类路径 39 | |-------------------------------------------------------------------------- 40 | */ 41 | 'proxy_dir' => BASE_PATH . '/runtime/container/proxy', 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | 设置路由前缀 46 | |-------------------------------------------------------------------------- 47 | */ 48 | 'prefix_url' => env('API_DOCS_PREFIX_URL', '/swagger'), 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | 设置swagger资源路径,cdn资源 53 | |-------------------------------------------------------------------------- 54 | */ 55 | 'prefix_swagger_resources' => 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.27.1', 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | 设置全局返回的代理类 60 | |-------------------------------------------------------------------------- 61 | | 62 | | 全局返回 如:[code=>200,data=>null] 格式,设置会后会全局生成对应文档 63 | | 配合ApiVariable注解使用,示例参考GlobalResponse类 64 | | 返回数据格式可以利用AOP统一返回 65 | | 66 | */ 67 | // 'global_return_responses_class' => GlobalResponse::class, 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | 替换验证属性 72 | |-------------------------------------------------------------------------- 73 | | 74 | | 通过获取注解ApiModelProperty的值,来提供数据验证的提示信息 75 | | 76 | */ 77 | 'validation_custom_attributes' => true, 78 | 79 | /* 80 | |-------------------------------------------------------------------------- 81 | | 设置DTO类默认值等级 82 | |-------------------------------------------------------------------------- 83 | | 84 | | 设置:0 默认(不设置默认值) 85 | | 设置:1 简单类型会为设置默认值,复杂类型(带?)会设置null 86 | | - 简单类型默认值: int:0 float:0 string:'' bool:false array:[] mixed:null 87 | | 设置:2 (慎用)包含等级1且复杂类型(联合类型除外)会设置null 88 | | 89 | */ 90 | 'dto_default_value_level' => 0, 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | 全局responses,映射到ApiResponse注解对象 95 | |-------------------------------------------------------------------------- 96 | */ 97 | 'responses' => [ 98 | ['response' => 401, 'description' => 'Unauthorized'], 99 | ['response' => 500, 'description' => 'System error'], 100 | ], 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | swagger 的基础配置 105 | |-------------------------------------------------------------------------- 106 | | 107 | | 该属性会映射到OpenAPI对象 108 | | 109 | */ 110 | 'swagger' => [ 111 | 'info' => [ 112 | 'title' => 'API DOC', 113 | 'version' => '0.1', 114 | 'description' => 'swagger api desc', 115 | ], 116 | 'servers' => [ 117 | [ 118 | 'url' => 'http://127.0.0.1:9501', 119 | 'description' => 'OpenApi host', 120 | ], 121 | ], 122 | 'components' => [ 123 | 'securitySchemes' => [ 124 | [ 125 | 'securityScheme' => 'Authorization', 126 | 'type' => 'apiKey', 127 | 'in' => 'header', 128 | 'name' => 'Authorization', 129 | ], 130 | ], 131 | ], 132 | 'security' => [ 133 | ['Authorization' => []], 134 | ], 135 | 'externalDocs' => [ 136 | 'description' => 'Find out more about Swagger', 137 | 'url' => 'https://github.com/tw2066/api-docs', 138 | ], 139 | ], 140 | ]; 141 | -------------------------------------------------------------------------------- /src/Swagger/SwaggerOpenApi.php: -------------------------------------------------------------------------------- 1 | openApi = new OpenApi(); 36 | $this->openApi->paths = []; 37 | $this->openApi->tags = []; 38 | $this->openApi->components = new OA\Components(); 39 | $this->tags = []; 40 | $this->queuePaths = new SplPriorityQueue(); 41 | $this->queueTags = new SplPriorityQueue(); 42 | $this->setOpenapiVersion(); 43 | $this->setInfo(); 44 | $this->setServers(); 45 | $this->setComponentsSecuritySchemes(); 46 | $this->setSecurity(); 47 | $this->setExternalDocs(); 48 | $this->serverNameAll[] = $serverName; 49 | } 50 | 51 | public function clean(): void 52 | { 53 | $this->openApi = null; 54 | $this->tags = []; 55 | $this->queuePaths = null; 56 | $this->queueTags = null; 57 | } 58 | 59 | public function getQueuePaths(): SplPriorityQueue 60 | { 61 | return $this->queuePaths; 62 | } 63 | 64 | /** 65 | * 设置OpenapiVersion. 66 | */ 67 | public function setOpenapiVersion(): void 68 | { 69 | if (! empty($this->swaggerConfig->getSwagger()['openapi'])) { 70 | $this->openApi->openapi = $this->swaggerConfig->getSwagger()['openapi']; 71 | } 72 | } 73 | 74 | /** 75 | * 设置openApi对象 security. 76 | */ 77 | public function setSecurity(): void 78 | { 79 | if (! empty($this->swaggerConfig->getSwagger()['security'])) { 80 | $this->openApi->security = $this->swaggerConfig->getSwagger()['security']; 81 | } 82 | } 83 | 84 | /** 85 | * 设置tags. 86 | */ 87 | public function setTags(string $tagName, int $position, Tag $tag): void 88 | { 89 | if (isset($this->tags[$tagName])) { 90 | return; 91 | } 92 | $this->tags[$tagName] = true; 93 | $this->queueTags->insert($tag, $position); 94 | } 95 | 96 | public function setComponentsSchemas(array $componentsSchemas): void 97 | { 98 | $this->componentsSchemas = array_values($componentsSchemas); 99 | } 100 | 101 | public function setComponentsSecuritySchemes(): void 102 | { 103 | $securitySchemes = $this->swaggerConfig->getSwagger()['components']['securitySchemes'] ?? []; 104 | if ($securitySchemes) { 105 | $this->openApi->components->securitySchemes = []; 106 | foreach ($securitySchemes as $securityScheme) { 107 | $this->openApi->components->securitySchemes[] = Mapper::map($securityScheme, new OA\SecurityScheme()); 108 | } 109 | } 110 | } 111 | 112 | public function save(string $serverName): void 113 | { 114 | // 设置paths 115 | $paths = []; 116 | while (! $this->queuePaths->isEmpty()) { 117 | /** @var OA\PathItem $pathItem */ 118 | [$pathItem,$method] = $this->queuePaths->extract(); 119 | $route = $pathItem->path; 120 | // 相同path不同method 121 | if (isset($paths[$route])) { 122 | $paths[$route]->{$method} = $pathItem->{$method}; 123 | } else { 124 | $paths[$route] = $pathItem; 125 | } 126 | } 127 | $this->openApi->paths = $paths; 128 | // 设置tags 129 | while (! $this->queueTags->isEmpty()) { 130 | $this->openApi->tags[] = $this->queueTags->extract(); 131 | } 132 | // 设置components->schemas 133 | $this->openApi->components->schemas = array_values($this->componentsSchemas); 134 | // 创建目录 135 | $outputDir = $this->swaggerConfig->getOutputDir(); 136 | if (file_exists($outputDir) === false) { 137 | if (mkdir($outputDir, 0755, true) === false) { 138 | throw new ApiDocsException("Failed to create a directory : {$outputDir}"); 139 | } 140 | } 141 | $outputFile = $outputDir . '/' . $serverName . '.' . $this->swaggerConfig->getFormat(); 142 | $this->openApi->saveAs($outputFile); 143 | } 144 | 145 | protected function setInfo(): void 146 | { 147 | $info = $this->swaggerConfig->getSwagger()['info'] ?? []; 148 | $this->openApi->info = Mapper::map($info, new OA\Info()); 149 | } 150 | 151 | protected function setExternalDocs(): void 152 | { 153 | $externalDocs = $this->swaggerConfig->getSwagger()['externalDocs'] ?? []; 154 | if ($externalDocs) { 155 | $this->openApi->externalDocs = Mapper::map($externalDocs, new OA\ExternalDocumentation()); 156 | } 157 | } 158 | 159 | protected function setServers(): void 160 | { 161 | $servers = $this->swaggerConfig->getSwagger()['servers'] ?? []; 162 | if ($servers) { 163 | $this->openApi->servers = Mapper::mapArray($servers, OA\Server::class); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Swagger/GenerateProxyClass.php: -------------------------------------------------------------------------------- 1 | swaggerConfig->getProxyDir(); 33 | if (file_exists($proxyDir) === false) { 34 | if (mkdir($proxyDir, 0755, true) === false) { 35 | throw new ApiDocsException("Failed to create a directory : {$proxyDir}"); 36 | } 37 | } 38 | } 39 | 40 | public function getApiVariableClass(string $newClassname) 41 | { 42 | $newClassname = trim($newClassname, '\\'); 43 | if ($this->apiVariableClassArr === null) { 44 | $arr = []; 45 | $classes = AnnotationCollector::getPropertiesByAnnotation(ApiVariable::class); 46 | foreach ($classes as $class) { 47 | $classname = $class['class']; 48 | $arr[$classname][] = $class['property']; 49 | } 50 | $this->apiVariableClassArr = $arr; 51 | } 52 | return $this->apiVariableClassArr[$newClassname] ?? []; 53 | } 54 | 55 | /** 56 | * 生成代理类. 57 | */ 58 | public function generate(object $obj): string 59 | { 60 | $ref = new ReflectionClass($obj); 61 | $classname = $obj::class; 62 | $properties = $this->getApiVariableClass($classname); 63 | if (empty($properties)) { 64 | return $classname; 65 | } 66 | 67 | $propertyArr = []; 68 | foreach ($properties as $property) { 69 | // 获取变量值 70 | $propertyValue = $obj->{$property}; 71 | 72 | $type = $this->swaggerCommon->getPhpType($propertyValue); 73 | if (is_object($propertyValue) && $type != '\stdClass') { 74 | $propertyClassname = $type; 75 | if ($this->getApiVariableClass($propertyClassname)) { 76 | $propertyClassname = '\\' . $this->generate($propertyValue); 77 | } 78 | $type = $propertyClassname; 79 | } 80 | $propertyArr[$property] = $type; 81 | if (is_array($propertyValue) && count($propertyValue) > 0) { 82 | $arrayType = $this->swaggerCommon->getPhpType($propertyValue[0]); 83 | if (is_object($propertyValue[0]) && $propertyValue[0]::class != '\stdClass') { 84 | $propertyClassname = $arrayType; 85 | if ($this->getApiVariableClass($propertyClassname)) { 86 | $propertyClassname = '\\' . $this->generate($propertyValue[0]); 87 | } 88 | $arrayType = $propertyClassname; 89 | } 90 | $propertyArr[$property] = [$arrayType]; 91 | } 92 | if (is_array($propertyValue) && count($propertyValue) == 0) { 93 | $propertyArr[$property] = 'array'; 94 | } 95 | } 96 | 97 | $file = new SplFileInfo($ref->getFileName()); 98 | $realPath = $file->getRealPath(); 99 | [$generateNamespaceClassName, $content] = $this->phpParser($obj, $realPath, $propertyArr); 100 | 101 | if (! isset($this->proxyClassArr[$generateNamespaceClassName])) { 102 | $this->putContents($generateNamespaceClassName, $content); 103 | $this->proxyClassArr[$generateNamespaceClassName] = $classname; 104 | } 105 | 106 | return $generateNamespaceClassName; 107 | } 108 | 109 | /** 110 | * 获取代理类对应的源类. 111 | */ 112 | public function getSourceClassname(string $proxyClassname): ?string 113 | { 114 | return $this->proxyClassArr[$proxyClassname] ?? null; 115 | } 116 | 117 | protected function putContents($generateNamespaceClassName, $content): void 118 | { 119 | $outputDir = $this->swaggerConfig->getProxyDir(); 120 | $generateClassName = str_replace('\\', '_', $generateNamespaceClassName); 121 | $filename = $outputDir . $generateClassName . '.dto.proxy.php'; 122 | if (! $this->dtoConfig->isScanCacheable()) { 123 | file_put_contents($filename, $content); 124 | } 125 | $classLoader = Composer::getLoader(); 126 | $classLoader->addClassMap([$generateNamespaceClassName => $filename]); 127 | } 128 | 129 | protected function phpParser(object $generateClass, $filePath, $propertyArr): array 130 | { 131 | $code = file_get_contents($filePath); 132 | $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); 133 | $ast = $parser->parse($code); 134 | 135 | $simpleClassName = $this->swaggerCommon->getSimpleClassName($generateClass::class); 136 | $generateClassName = $simpleClassName; 137 | foreach ($propertyArr as $type) { 138 | if (is_array($type)) { 139 | $generateClassName .= 'Array'; 140 | $type = $type[0]; 141 | } 142 | $type = $this->swaggerCommon->getSimpleClassName($type); 143 | $generateClassName .= $type; 144 | } 145 | $fullGenerateClassName = 'ApiDocs\Proxy\\' . $generateClassName; 146 | if (isset($this->proxyClassArr[$fullGenerateClassName])) { 147 | return [$fullGenerateClassName, '']; 148 | } 149 | 150 | $traverser = new NodeTraverser(); 151 | $resVisitor = make(ResponseVisitor::class, [$generateClass, $generateClassName, $propertyArr]); 152 | $traverser->addVisitor($resVisitor); 153 | $ast = $traverser->traverse($ast); 154 | 155 | $prettyPrinter = new PrettyPrinter\Standard(); 156 | $content = $prettyPrinter->prettyPrintFile($ast); 157 | return [$fullGenerateClassName, $content]; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Swagger/SwaggerComponents.php: -------------------------------------------------------------------------------- 1 | [], 'requiredArr' => []]; 47 | } 48 | 49 | $rc = ReflectionManager::reflectClass($className); 50 | $propertyArr = []; 51 | $requiredArr = []; 52 | // 循环字段 53 | foreach ($rc->getProperties() as $reflectionProperty) { 54 | // 属性 55 | $property = new OA\Property(); 56 | $fieldName = $reflectionProperty->getName(); 57 | $propertyManager = $this->propertyManager->getProperty($className, $fieldName); 58 | 59 | // 适配ApiVariable注解 60 | $sourceClassName = $this->generateProxyClass?->getSourceClassname($className) ?? $className; 61 | $apiModelProperty = ApiAnnotation::getProperty($sourceClassName, $fieldName, ApiModelProperty::class) ?: new ApiModelProperty(); 62 | 63 | /** @var In $inAnnotation */ 64 | $inAnnotation = ApiAnnotation::getProperty($sourceClassName, $fieldName, In::class)?->toAnnotations()[0]; 65 | if ($apiModelProperty->hidden) { 66 | continue; 67 | } 68 | if (! $reflectionProperty->isPublic() 69 | && ! $rc->hasMethod(setter($fieldName)) 70 | && ! $rc->hasMethod(DtoConfig::getDtoAliasMethodName($fieldName)) 71 | ) { 72 | continue; 73 | } 74 | 75 | // 字段名称 76 | $property->property = $propertyManager->alias ?? $fieldName; 77 | // 描述 78 | $apiModelProperty->value !== null && $property->description = $apiModelProperty->value; 79 | // required 80 | /** @var Required $requiredAnnotation */ 81 | $requiredAnnotation = ApiAnnotation::getProperty($sourceClassName, $fieldName, Required::class)?->toAnnotations()[0]; 82 | if ($apiModelProperty->required || $requiredAnnotation) { 83 | $requiredArr[] = $fieldName; 84 | } 85 | $property->example = $apiModelProperty->example; 86 | $property->default = $this->common->getPropertyDefaultValue($className, $reflectionProperty); 87 | 88 | $isSimpleType = $propertyManager->isSimpleType; 89 | $phpSimpleType = $propertyManager->phpSimpleType; 90 | if ($apiModelProperty->phpType) { 91 | $isSimpleType = true; 92 | $phpSimpleType = $apiModelProperty->phpType; 93 | } 94 | 95 | // swagger 类型 96 | $swaggerType = $this->common->getSwaggerType($phpSimpleType); 97 | 98 | // 枚举:in 99 | if (! empty($inAnnotation) && empty($apiModelProperty->phpType)) { 100 | $property->type = $swaggerType; 101 | $property->enum = $inAnnotation->getValue(); 102 | } 103 | // 简单类型 104 | elseif ($isSimpleType) { 105 | // 数组 106 | if ($swaggerType == 'array') { 107 | $property->type = 'array'; 108 | $items = new OA\Items(); 109 | $items->type = 'null'; 110 | $property->items = $items; 111 | } else { 112 | // 普通简单类型 113 | $property->type = $swaggerType; 114 | } 115 | } // 枚举类型 116 | elseif ($propertyManager->enum) { 117 | $property->type = $this->common->getSwaggerType($propertyManager->enum->backedType); 118 | $property->enum = $propertyManager->enum->valueList; 119 | } // 普通类 120 | else { 121 | if ($swaggerType == 'array') { 122 | $property->type = 'array'; 123 | if (! empty($propertyManager->arrClassName)) { 124 | $items = new OA\Items(); 125 | $items->ref = $this->common->getComponentsName($propertyManager->arrClassName); 126 | $property->items = $items; 127 | $this->generateSchemas($propertyManager->arrClassName); 128 | $property->default = Generator::UNDEFINED; 129 | } elseif (! empty($propertyManager->arrSimpleType)) { 130 | $items = new OA\Items(); 131 | $items->type = $this->common->getSwaggerType($propertyManager->arrSimpleType); 132 | $property->items = $items; 133 | } 134 | } elseif (! empty($propertyManager->className)) { 135 | $property->ref = $this->common->getComponentsName($propertyManager->className); 136 | $this->generateSchemas($propertyManager->className); 137 | } else { 138 | throw new ApiDocsException("field:{$className}-{$fieldName} type resolved not found"); 139 | } 140 | } 141 | $propertyArr[] = $property; 142 | } 143 | return ['propertyArr' => $propertyArr, 'requiredArr' => $requiredArr]; 144 | } 145 | 146 | public function generateSchemas(string $className) 147 | { 148 | $simpleClassName = $this->common->getSimpleClassName($className); 149 | if (isset(static::$schemas[$simpleClassName])) { 150 | return static::$schemas[$simpleClassName]; 151 | } 152 | $schema = new OA\Schema(); 153 | $schema->schema = $simpleClassName; 154 | 155 | $data = $this->getProperties($className); 156 | $schema->properties = $data['propertyArr']; 157 | /** @var ApiModel $apiModel */ 158 | $apiModel = AnnotationCollector::getClassAnnotation($className, ApiModel::class); 159 | if ($apiModel) { 160 | $schema->description = $apiModel->value; 161 | } 162 | $data['requiredArr'] && $schema->required = $data['requiredArr']; 163 | self::$schemas[$simpleClassName] = $schema; 164 | return self::$schemas[$simpleClassName]; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Swagger/GenerateResponses.php: -------------------------------------------------------------------------------- 1 | methodDefinitionCollector->getReturnType($this->className, $this->methodName); 38 | $returnTypeClassName = $definition->getName(); 39 | // 全局 40 | $globalResp = $this->getGlobalResp(); 41 | // 注解 42 | $annotationResp = $this->getAnnotationResp(); 43 | $arr = []; 44 | 45 | $code = $this->swaggerConfig->getResponsesCode(); 46 | $response = new OA\Response(); 47 | $response->response = $code; 48 | $response->description = 'successful operation'; 49 | $content = $this->getContent($returnTypeClassName); 50 | $content && $response->content = $content; 51 | $arr[$code] = $response; 52 | 53 | $annotationResp && $arr = Arr::merge($arr, $annotationResp); 54 | $globalResp && $arr = Arr::merge($arr, $globalResp); 55 | 56 | return array_values($arr); 57 | } 58 | 59 | // protected function getReturnJsonContent(string $returnTypeClassName, bool $isArray = false): array 60 | // { 61 | // $arr = []; 62 | // $mediaType = new OA\MediaType(); 63 | // $mediaTypeStr = 'application/json'; 64 | // $mediaType->schema = $this->getJsonContent($returnTypeClassName, $isArray); 65 | // $arr[$mediaTypeStr] = $mediaType; 66 | // $mediaType->mediaType = $mediaTypeStr; 67 | // return $arr; 68 | // } 69 | 70 | protected function getContent(array|object|string $returnTypeClassName): array 71 | { 72 | // 获取全局类 73 | $globalReturnResponsesClass = $this->swaggerConfig->getGlobalReturnResponsesClass(); 74 | if ($globalReturnResponsesClass) { 75 | $returnTypeClassName = make($globalReturnResponsesClass, [$returnTypeClassName]); 76 | } 77 | // 判断对象 78 | if (is_object($returnTypeClassName)) { 79 | // 生成代理类 80 | if ($this->genericProxyClass->getApiVariableClass($returnTypeClassName::class)) { 81 | $returnTypeClassName = $this->genericProxyClass->generate($returnTypeClassName); 82 | } else { 83 | $returnTypeClassName = $returnTypeClassName::class; 84 | } 85 | } 86 | 87 | $isArray = is_array($returnTypeClassName); 88 | if ($isArray) { 89 | $returnTypeClassName = $returnTypeClassName[0] ?? null; 90 | $returnTypeClassName = is_object($returnTypeClassName) ? $returnTypeClassName::class : $returnTypeClassName; 91 | } 92 | $returnTypeClassName == 'array' && $isArray = true; 93 | $arr = []; 94 | $mediaType = new OA\MediaType(); 95 | $mediaTypeStr = 'text/plain'; 96 | // 简单类型 97 | if ($this->common->isSimpleType($returnTypeClassName)) { 98 | $schema = new OA\Schema(); 99 | $schema->type = $this->common->getSwaggerType($returnTypeClassName); 100 | // 数组 101 | if ($isArray) { 102 | $mediaTypeStr = 'application/json'; 103 | $schema->type = 'array'; 104 | $items = new OA\Items(); 105 | $swaggerType = $this->common->getSwaggerType($returnTypeClassName); 106 | $items->type = $swaggerType == 'array' ? 'null' : $swaggerType; 107 | $schema->items = $items; 108 | } 109 | $mediaType->schema = $schema; 110 | } elseif ($this->container->has($returnTypeClassName)) { 111 | $mediaTypeStr = 'application/json'; 112 | $mediaType->schema = $this->getJsonContent($returnTypeClassName, $isArray); 113 | } else { 114 | // $schema = new OA\Schema(); 115 | // $schema->type = 'null'; 116 | // $mediaType->schema = $schema; 117 | // 其他类型数据 eg:mixed 118 | return []; 119 | } 120 | 121 | $arr[$mediaTypeStr] = $mediaType; 122 | $mediaType->mediaType = $mediaTypeStr; 123 | return $arr; 124 | } 125 | 126 | /** 127 | * 获取返回类型的JsonContent. 128 | */ 129 | protected function getJsonContent(string $returnTypeClassName, bool $isArray): OA\JsonContent 130 | { 131 | $jsonContent = new OA\JsonContent(); 132 | $this->swaggerComponents->generateSchemas($returnTypeClassName); 133 | 134 | if ($isArray) { 135 | $jsonContent->type = 'array'; 136 | $items = new OA\Items(); 137 | $items->ref = $this->common->getComponentsName($returnTypeClassName); 138 | $jsonContent->items = $items; 139 | } else { 140 | $jsonContent->ref = $this->common->getComponentsName($returnTypeClassName); 141 | } 142 | 143 | return $jsonContent; 144 | } 145 | 146 | /** 147 | * 获得全局Response. 148 | */ 149 | protected function getGlobalResp(): array 150 | { 151 | $resp = []; 152 | foreach ($this->swaggerConfig->getResponses() as $value) { 153 | $apiResponse = new ApiResponse(); 154 | $apiResponse->response = $value['response'] ?? null; 155 | $apiResponse->description = $value['description'] ?? null; 156 | ! empty($value['returnType']) && $apiResponse->returnType = $value['returnType']; 157 | $resp[$apiResponse->response] = $this->getOAResp($apiResponse); 158 | } 159 | return $resp; 160 | } 161 | 162 | protected function getOAResp(ApiResponse $apiResponse): OA\Response 163 | { 164 | $response = new OA\Response(); 165 | $response->response = $apiResponse->response; 166 | $response->description = $apiResponse->description; 167 | if (! empty($apiResponse->returnType)) { 168 | $returnType = $apiResponse->returnType; 169 | $content = $this->getContent($returnType); 170 | $content && $response->content = $content; 171 | } 172 | return $response; 173 | } 174 | 175 | /** 176 | * 获取注解上的Response. 177 | * @return OA\Response[] 178 | */ 179 | protected function getAnnotationResp(): array 180 | { 181 | $resp = []; 182 | /** @var ApiResponse $apiResponse */ 183 | foreach ($this->apiResponseArr as $apiResponse) { 184 | $resp[$apiResponse->response] = $this->getOAResp($apiResponse); 185 | } 186 | return $resp; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /README2.md: -------------------------------------------------------------------------------- 1 | ## PHP Swagger Api Docs 2 | 基于 [Hyperf](https://github.com/hyperf/hyperf) 框架的 swagger 文档生成组件,支持swoole/swow驱动 3 | 4 | ##### 优点 5 | 6 | - 声明参数类型完成自动注入,参数映射到PHP类,根据类和注解自动生成Swagger文档 7 | - 代码DTO模式,可维护性好,扩展性好 8 | - 支持数组(类/简单类型),递归,嵌套 9 | - 支持注解数据校验 10 | - 支持api token 11 | - 支持PHP8原生注解,PHP8.1枚举 12 | - 支持openapi 3.0 13 | 14 | ## 使用须知 15 | 16 | * php版本 >= 8.0 17 | * 控制器中方法尽可能返回类,这样会更好的生成文档 18 | * 当返回类的结果满足不了时,可以使用 #[ApiResponse] 注解 19 | 20 | ## 例子 21 | > 请参考[example目录](https://github.com/tw2066/api-docs/tree/master/example) 22 | ## 安装 23 | 24 | ``` 25 | composer require tangwei/apidocs 26 | ``` 27 | 28 | ## 使用 29 | 30 | #### 1. 发布配置文件 31 | 32 | ```bash 33 | php bin/hyperf.php vendor:publish tangwei/apidocs 34 | ``` 35 | ##### 1.1 配置信息 36 | > config/autoload/api_docs.php 37 | ```php 38 | return [ 39 | // enable false 将不会启动 swagger 服务 40 | 'enable' => env('APP_ENV') !== 'prod', 41 | 'format' => 'json', 42 | 'output_dir' => BASE_PATH . '/runtime/swagger', 43 | 'prefix_url' => env('API_DOCS_PREFIX_URL', '/swagger'), 44 | // 替换验证属性 45 | 'validation_custom_attributes' => true, 46 | // 全局responses 47 | 'responses' => [ 48 | ['response' => 401, 'description' => 'Unauthorized'], 49 | ['response' => 500, 'description' => 'System error'], 50 | ], 51 | // swagger 的基础配置 OpenAPI 对象 52 | 'swagger' => [ 53 | 'info' => [ 54 | 'title' => 'API DOC', 55 | 'version' => '0.1', 56 | 'description' => 'swagger api desc', 57 | ], 58 | 'servers' => [ 59 | [ 60 | 'url' => 'http://127.0.0.1:9501', 61 | 'description' => 'OpenApi host', 62 | ], 63 | ], 64 | 'components' => [ 65 | 'securitySchemes' => [ 66 | [ 67 | 'securityScheme' => 'Authorization', 68 | 'type' => 'apiKey', 69 | 'in' => 'header', 70 | 'name' => 'Authorization', 71 | ], 72 | ], 73 | ], 74 | 'security' => [ 75 | ['Authorization' => []], 76 | ], 77 | 'externalDocs' => [ 78 | 'description' => 'Find out more about Swagger', 79 | 'url' => 'https://github.com/tw2066/api-docs', 80 | ], 81 | ], 82 | ]; 83 | ``` 84 | 85 | ### 2. 直接启动框架(需要有http服务) 86 | 87 | ```shell script 88 | php bin/hyperf.php start 89 | 90 | [INFO] Swagger docs url at http://0.0.0.0:9501/swagger 91 | [INFO] TaskWorker#1 started. 92 | [INFO] Worker#0 started. 93 | [INFO] HTTP Server listening at 0.0.0.0:9501 94 | ``` 95 | 96 | > 看到`Swagger Url`显示,表示文档生成成功,访问`/swagger`即可以看到swagger页面 97 | 98 | ### 3. 使用 99 | 100 | ## 注解 101 | 102 | > 命名空间:`Hyperf\DTO\Annotation\Contracts` 103 | 104 | #### #[RequestBody] 105 | 106 | - 获取Body参数 107 | 108 | ```php 109 | public function add(#[RequestBody] DemoBodyRequest $request){} 110 | ``` 111 | 112 | #### #[RequestQuery] 113 | 114 | - 获取GET参数 115 | 116 | ```php 117 | public function add(#[RequestQuery] DemoQuery $request){} 118 | ``` 119 | 120 | #### #[RequestFormData] 121 | 122 | - 获取表单请求 123 | 124 | ```php 125 | public function fromData(#[RequestFormData] DemoFormData $formData){} 126 | ``` 127 | 128 | - 获取文件(和表单一起使用) 129 | 130 | ```php 131 | #[ApiFormData(name: 'photo', format: 'binary')] 132 | ``` 133 | 134 | - 获取Body参数和GET参数 135 | 136 | ```php 137 | public function add(#[RequestBody] DemoBodyRequest $request, #[RequestQuery] DemoQuery $query){} 138 | ``` 139 | 140 | #### #[ApiSecurity] 注解 141 | - 优先级: 方法 > 类 > 全局 142 | ```php 143 | #[ApiSecurity('Authorization')] 144 | public function getUserInfo(DemoToken $header){} 145 | ``` 146 | 147 | > 注意: 一个方法,不能同时注入RequestBody和RequestFormData 148 | 149 | ## 示例 150 | 151 | ### 控制器 152 | 153 | ```php 154 | #[Controller(prefix: '/demo')] 155 | #[Api(tags: 'demo管理', position: 1)] 156 | class DemoController extends AbstractController 157 | { 158 | #[ApiOperation(summary: '查询')] 159 | #[PostMapping(path: 'index')] 160 | public function index(#[RequestQuery] #[Valid] DemoQuery $request): Contact 161 | { 162 | $contact = new Contact(); 163 | $contact->name = $request->name; 164 | var_dump($request); 165 | return $contact; 166 | } 167 | 168 | #[PutMapping(path: 'add')] 169 | #[ApiOperation(summary: '提交body数据和get参数')] 170 | public function add(#[RequestBody] DemoBodyRequest $request, #[RequestQuery] DemoQuery $query) 171 | { 172 | var_dump($query); 173 | return json_encode($request, JSON_UNESCAPED_UNICODE); 174 | } 175 | 176 | #[PostMapping(path: 'fromData')] 177 | #[ApiOperation(summary: '表单提交')] 178 | #[ApiFormData(name: 'photo', type: 'file')] 179 | #[ApiResponse(code: '200', description: 'success', className: Address::class, type: 'array')] 180 | public function fromData(#[RequestFormData] DemoFormData $formData): bool 181 | { 182 | $file = $this->request->file('photo'); 183 | var_dump($file); 184 | var_dump($formData); 185 | return true; 186 | } 187 | 188 | #[GetMapping(path: 'find/{id}/and/{in}')] 189 | #[ApiOperation('查询单体记录')] 190 | #[ApiHeader(name: 'test')] 191 | public function find(int $id, float $in): array 192 | { 193 | return ['$id' => $id, '$in' => $in]; 194 | } 195 | 196 | } 197 | 198 | ``` 199 | 200 | ## 验证器 201 | 202 | ### 基于框架的验证 203 | 204 | > 安装hyperf框架验证器[hyperf/validation](https://github.com/hyperf/validation), 并配置(已安装忽略) 205 | 206 | - 注解 207 | `Required` `Between` `Date` `Email` `Image` `Integer` `Nullable` `Numeric` `Url` `Validation` `...` 208 | - 校验生效 209 | 210 | > 只需在控制器方法中加上 #[Valid] 注解 211 | 212 | ```php 213 | public function index(#[RequestQuery] #[Valid] DemoQuery $request){} 214 | ``` 215 | 216 | ```php 217 | class DemoQuery 218 | { 219 | #[ApiModelProperty('名称')] 220 | #[Required] 221 | #[Max(5)] 222 | #[In(['qq','aa'])] 223 | public string $name; 224 | 225 | #[ApiModelProperty('正则')] 226 | #[Str] 227 | #[Regex('/^.+@.+$/i')] 228 | #[StartsWith('aa,bb')] 229 | #[Max(10)] 230 | public string $email; 231 | 232 | #[ApiModelProperty('数量')] 233 | #[Required] 234 | #[Integer] 235 | #[Between(1,5)] 236 | public int $num; 237 | } 238 | ``` 239 | 240 | ### 自定义注解验证 241 | 242 | > 注解的验证支持框架所有验证, 组件提供了常用的注解用于验证 243 | 244 | 1. 使用自定义验证注解, 创建注解类继承`Hyperf\DTO\Annotation\Validation\BaseValidation` 245 | 2. 重写`$rule`属性或`getRule`方法 246 | ```php 247 | //示例 248 | #[Attribute(Attribute::TARGET_PROPERTY)] 249 | class Image extends BaseValidation 250 | { 251 | protected $rule = 'image'; 252 | } 253 | ``` 254 | 255 | ### 验证器Validation 256 | 257 | 1. 大家都习惯了框架的`required|date|after:start_date`写法 258 | ```php 259 | //可以通过Validation实现 260 | #[Validation('required|date|after:start_date')] 261 | ``` 262 | 2. 需要支持数组里面是int数据情况 `'intArr.*' => 'integer'`的情况 263 | ```php 264 | //可以通过Validation中customKey来自定义key实现 265 | #[Validation('integer', customKey: 'intArr.*')] 266 | public array $intArr; 267 | ``` 268 | 上面写法和`'intArr.*' => 'integer'`效果相同 269 | 270 | ## 注意 271 | ### 数组类型的问题 272 | > PHP原生暂不支持`int[]`或`Class[]`类型, 使用示例 273 | ```php 274 | /** 275 | * class类型映射数组. 276 | * @var \App\DTO\Address[] 277 | */ 278 | #[ApiModelProperty('地址')] 279 | public array $addressArr; 280 | 281 | /** 282 | * 简单类型映射数组. 283 | * @var int[] 284 | */ 285 | #[ApiModelProperty('int类型的数组')] 286 | public array $intArr; 287 | 288 | /** 289 | * 通过注解映射数组. 290 | */ 291 | #[ApiModelProperty('string类型的数组')] 292 | #[ArrayType('string')] 293 | public array $stringArr; 294 | ``` 295 | 296 | ### hyperf 2.2版本报错 297 | > @required注解会提示报错,请忽略required 298 | > 299 | > 修改文件config/autoload/annotations.php 300 | ```php 301 | return [ 302 | 'scan' => [ 303 | //... 304 | 'ignore_annotations' => [ 305 | //... 306 | 'required' 307 | ], 308 | ], 309 | ]; 310 | ``` 311 | 312 | ### `AutoController`注解 313 | > 控制器中使用`AutoController`注解,只收集了`POST`方法 314 | > 315 | ## RPC 返回PHP对象 316 | > 当框架导入 symfony/serializer (^5.0) 和 symfony/property-access (^5.0) 后,并在 dependencies.php 中配置一下映射关系 317 | ```php 318 | use Hyperf\DTO\Serializer\SerializerFactory; 319 | use Hyperf\Utils\Serializer\Serializer; 320 | 321 | return [ 322 | Hyperf\Contract\NormalizerInterface::class => new SerializerFactory(Serializer::class), 323 | ]; 324 | ``` 325 | ## Swagger界面 326 | ![hMvJnQ](https://gitee.com/tw666/source/raw/master/img/swagger.png) 327 | 328 | 329 | 330 | -------------------------------------------------------------------------------- /src/Swagger/SwaggerPaths.php: -------------------------------------------------------------------------------- 1 | getMethodNamePosition($className, $methodName); 52 | $classAnnotation = ApiAnnotation::classMetadata($className); 53 | /** @var Api $apiControllerAnnotation */ 54 | $apiControllerAnnotation = $classAnnotation[Api::class] ?? new Api(); 55 | if ($apiControllerAnnotation->hidden) { 56 | return; 57 | } 58 | 59 | // AutoController Validation POST 60 | $autoControllerAnnotation = $classAnnotation[AutoController::class] ?? null; 61 | if ($autoControllerAnnotation && $methods != 'POST') { 62 | return; 63 | } 64 | $methodAnnotations = AnnotationCollector::getClassMethodAnnotation($className, $methodName); 65 | $apiOperation = $methodAnnotations[ApiOperation::class] ?? new ApiOperation(); 66 | if ($apiOperation->hidden) { 67 | return; 68 | } 69 | 70 | $apiHeaderControllerAnnotation = isset($classAnnotation[ApiHeader::class]) ? $classAnnotation[ApiHeader::class]->toAnnotations() : []; 71 | $apiHeaderArr = isset($methodAnnotations[ApiHeader::class]) ? $methodAnnotations[ApiHeader::class]->toAnnotations() : []; 72 | $apiHeaderArr = array_merge($apiHeaderControllerAnnotation, $apiHeaderArr); 73 | 74 | $apiFormDataArr = isset($methodAnnotations[ApiFormData::class]) ? $methodAnnotations[ApiFormData::class]->toAnnotations() : []; 75 | $apiResponseArr = isset($methodAnnotations[ApiResponse::class]) ? $methodAnnotations[ApiResponse::class]->toAnnotations() : []; 76 | 77 | $simpleClassName = $this->common->getSimpleClassName($className); 78 | if (is_array($apiControllerAnnotation->tags)) { 79 | $tags = $apiControllerAnnotation->tags; 80 | } elseif (! empty($apiControllerAnnotation->tags) && is_string($apiControllerAnnotation->tags)) { 81 | $tags = [$apiControllerAnnotation->tags]; 82 | } else { 83 | $tags = [$simpleClassName]; 84 | } 85 | 86 | foreach ($tags as $tag) { 87 | $oaTag = new OA\Tag(); 88 | $oaTag->name = $tag; 89 | $oaTag->description = $apiControllerAnnotation->description ?: $simpleClassName; 90 | $this->swaggerOpenApi->setTags($tag, $apiControllerAnnotation->position, $oaTag); 91 | } 92 | 93 | $method = strtolower($methods); 94 | /** @var GenerateParameters $generateParameters */ 95 | $generateParameters = make(GenerateParameters::class, [$className, $methodName, $apiHeaderArr, $apiFormDataArr]); 96 | /** @var GenerateResponses $generateResponses */ 97 | $generateResponses = make(GenerateResponses::class, [$className, $methodName, $apiResponseArr]); 98 | $parameters = $generateParameters->generate(); 99 | $pathItem->path = $route; 100 | 101 | $OAClass = 'OpenApi\Attributes\\' . ucfirst($method); 102 | /** @var Operation $operation */ 103 | $operation = new $OAClass(); 104 | $operation->path = $route; 105 | $operation->tags = $tags; 106 | $operation->summary = $apiOperation->summary ?: Generator::UNDEFINED; 107 | $operation->description = $this->getClassMethodPath($className, $methodName); 108 | if ($apiOperation->description) { 109 | $operation->description .= '
' . $apiOperation->description; 110 | } 111 | $operation->operationId = $this->getOperationId($route, $methods); 112 | 113 | // 设置弃用 114 | $apiOperation->deprecated && $operation->deprecated = true; 115 | 116 | $parameters['requestBody'] && $operation->requestBody = $parameters['requestBody']; 117 | $parameters['parameter'] && $operation->parameters = $parameters['parameter']; 118 | 119 | $operation->responses = $generateResponses->generate(); 120 | 121 | // 安全验证 122 | $securityArr = $this->getSecurity($classAnnotation, $methodAnnotations); 123 | if ($securityArr !== false) { 124 | $operation->security = $securityArr; 125 | } 126 | 127 | $pathItem->{$method} = $operation; 128 | // 将$pathItem insert 129 | $this->swaggerOpenApi->getQueuePaths()->insert([$pathItem, $method], 0 - $position); 130 | } 131 | 132 | /** 133 | * 获取类方法路径(快速定位后端代码). 134 | */ 135 | protected function getClassMethodPath(string $fullClassName, string $methodName): string 136 | { 137 | $parts = explode('\\', $fullClassName); 138 | $shortParts = []; 139 | for ($i = 0; $i < count($parts) - 1; ++$i) { 140 | $shortParts[] = $parts[$i][0] ?? ''; 141 | } 142 | $shortParts[] = end($parts); 143 | return sprintf('%s', implode('.', $shortParts) . '::' . $methodName); 144 | } 145 | 146 | /** 147 | * 获取全局操作ID. 148 | */ 149 | protected function getOperationId(string $route, string $methods): string 150 | { 151 | $operationId = Str::camel(str_replace('/', '_', $route)); 152 | if (empty($operationId)) { 153 | $operationId = '-'; 154 | } 155 | if (! isset(self::$operationIds[$operationId])) { 156 | self::$operationIds[$operationId] = true; 157 | return $operationId; 158 | } 159 | return $this->getOperationId($operationId . ucfirst(strtolower($methods)), $methods); 160 | } 161 | 162 | /** 163 | * 获取安全认证 164 | */ 165 | protected function getSecurity(?array $classAnnotation, ?array $methodAnnotations): array|false 166 | { 167 | $apiOperation = $methodAnnotations[ApiOperation::class] ?? new ApiOperation(); 168 | if (! $apiOperation->security) { 169 | return []; 170 | } 171 | 172 | // 方法设置了 173 | $isMethodSetApiSecurity = $this->isSetApiSecurity($methodAnnotations); 174 | if ($isMethodSetApiSecurity) { 175 | return $this->setSecurity($methodAnnotations); 176 | } 177 | 178 | // 类上设置了 179 | $isClassSetApiSecurity = $this->isSetApiSecurity($classAnnotation); 180 | if ($isClassSetApiSecurity) { 181 | return $this->setSecurity($classAnnotation); 182 | } 183 | 184 | return false; 185 | } 186 | 187 | protected function isSetApiSecurity(?array $annotations): bool 188 | { 189 | foreach ($annotations ?? [] as $item) { 190 | if ($item instanceof MultipleAnnotationInterface) { 191 | $toAnnotations = $item->toAnnotations(); 192 | foreach ($toAnnotations as $annotation) { 193 | if ($annotation instanceof ApiSecurity) { 194 | return true; 195 | } 196 | } 197 | } 198 | } 199 | return false; 200 | } 201 | 202 | /** 203 | * 设置安全验证 204 | */ 205 | protected function setSecurity(?array $annotations): array 206 | { 207 | $result = []; 208 | foreach ($annotations ?? [] as $item) { 209 | if ($item instanceof MultipleAnnotationInterface) { 210 | $toAnnotations = $item->toAnnotations(); 211 | foreach ($toAnnotations as $annotation) { 212 | if ($annotation instanceof ApiSecurity) { 213 | // 存在需要设置的security 214 | if (! empty($annotation->name)) { 215 | $result[][$annotation->name] = $annotation->value; 216 | } 217 | } 218 | } 219 | } 220 | } 221 | return $result; 222 | } 223 | 224 | /** 225 | * 获取方法在类中的位置. 226 | */ 227 | protected function getMethodNamePosition(string $className, string $methodName): int 228 | { 229 | $methodArray = $this->makeMethodIndex($className); 230 | return $methodArray[$methodName] ?? 0; 231 | } 232 | 233 | /** 234 | * 设置位置并获取类位置数组. 235 | */ 236 | protected function makeMethodIndex(string $className): array 237 | { 238 | if (isset($this->classMethodArray[$className])) { 239 | return $this->classMethodArray[$className]; 240 | } 241 | $methodArray = ApiAnnotation::methodMetadata($className); 242 | foreach ($methodArray as $k => $item) { 243 | $methodArray[$k] = $this->index; 244 | ++$this->index; 245 | } 246 | $this->classMethodArray[$className] = $methodArray; 247 | return $methodArray; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/Swagger/GenerateParameters.php: -------------------------------------------------------------------------------- 1 | [], 47 | ]; 48 | // FormData类名 49 | $requestFormDataclass = ''; 50 | $parameterArr = $this->getParameterArrByBaseParam($this->apiHeaderArr); 51 | $definitions = $this->methodDefinitionCollector->getParameters($this->controller, $this->action); 52 | foreach ($definitions as $definition) { 53 | // query path 54 | $parameterClassName = $definition->getName(); 55 | $paramName = $definition->getMeta('name'); 56 | // 判断是否为简单类型 57 | $simpleSwaggerType = $this->common->getSimpleType2SwaggerType($parameterClassName); 58 | if ($simpleSwaggerType !== null) { 59 | $parameter = new OA\Parameter(); 60 | $parameter->required = true; 61 | $parameter->name = $paramName; 62 | $parameter->in = 'path'; 63 | $schema = new OA\Schema(); 64 | $schema->type = $simpleSwaggerType; 65 | $parameter->schema = $schema; 66 | $parameterArr[] = $parameter; 67 | continue; 68 | } 69 | 70 | if ($this->container->has($parameterClassName)) { 71 | $methodParameter = $this->methodParametersManager->getMethodParameter($this->controller, $this->action, $paramName); 72 | if ($methodParameter == null) { 73 | continue; 74 | } 75 | 76 | if ($methodParameter->isRequestBody()) { 77 | $requestBody = new OA\RequestBody(); 78 | $requestBody->required = true; 79 | // $requestBody->description = ''; 80 | $requestBody->content = $this->getContent($parameterClassName); 81 | $result['requestBody'] = $requestBody; 82 | } 83 | if ($methodParameter->isRequestQuery()) { 84 | $parameterArr = array_merge($parameterArr, $this->getParameterArrByClass($parameterClassName, 'query')); 85 | } 86 | if ($methodParameter->isRequestHeader()) { 87 | $parameterArr = array_merge($parameterArr, $this->getParameterArrByClass($parameterClassName, 'header')); 88 | } 89 | if ($methodParameter->isRequestFormData()) { 90 | $requestFormDataclass = $parameterClassName; 91 | } 92 | } 93 | } 94 | // Form表单 95 | if (! empty($requestFormDataclass) || ! empty($this->apiFormDataArr)) { 96 | $requestBody = new OA\RequestBody(); 97 | $requestBody->required = true; 98 | // $requestBody->description = ''; 99 | $mediaType = new OA\MediaType(); 100 | $mediaType->mediaType = 'multipart/form-data'; 101 | // $parameterClassName 102 | $mediaType->schema = $this->generateFormDataSchemas($requestFormDataclass, $this->apiFormDataArr); 103 | $mediaType->schema->type = 'object'; 104 | $requestBody->content = []; 105 | $requestBody->content[$mediaType->mediaType] = $mediaType; 106 | $result['requestBody'] = $requestBody; 107 | } 108 | 109 | $result['parameter'] = $parameterArr; 110 | return $result; 111 | } 112 | 113 | public function generateFormDataSchemas($className, $apiFormDataArr): OA\Schema 114 | { 115 | $schema = new OA\Schema(); 116 | $data = $this->swaggerComponents->getProperties($className); 117 | $annotationData = $this->getPropertiesByBaseParam($apiFormDataArr); 118 | $schema->properties = Arr::merge($data['propertyArr'], $annotationData['propertyArr']); 119 | $schema->required = Arr::merge($data['requiredArr'], $annotationData['requiredArr']); 120 | return $schema; 121 | } 122 | 123 | public function getParameterArrByClass(string $parameterClassName, string $in): array 124 | { 125 | $parameters = []; 126 | $rc = ReflectionManager::reflectClass($parameterClassName); 127 | foreach ($rc->getProperties() ?? [] as $reflectionProperty) { 128 | $propertyManager = $this->propertyManager->getProperty($parameterClassName, $reflectionProperty->name); 129 | $parameter = new OA\Parameter(); 130 | $fieldName = $reflectionProperty->getName(); 131 | $schema = new OA\Schema(); 132 | $parameter->name = $propertyManager?->alias ?? $fieldName; 133 | $parameter->in = $in; 134 | $schema->default = $this->common->getPropertyDefaultValue($parameterClassName, $reflectionProperty); 135 | 136 | $apiModelProperty = ApiAnnotation::getProperty($parameterClassName, $fieldName, ApiModelProperty::class); 137 | $apiModelProperty = $apiModelProperty ?: new ApiModelProperty(); 138 | if ($apiModelProperty->hidden) { 139 | continue; 140 | } 141 | if (! $reflectionProperty->isPublic() 142 | && ! $rc->hasMethod(\Hyperf\Support\setter($fieldName)) 143 | && ! $rc->hasMethod(DtoConfig::getDtoAliasMethodName($fieldName)) 144 | ) { 145 | continue; 146 | } 147 | // 存在自定义简单类型 148 | if ($apiModelProperty->phpType) { 149 | $phpType = $apiModelProperty->phpType; 150 | } else { 151 | $phpType = $this->common->getTypeName($reflectionProperty); 152 | $enum = $propertyManager?->enum; 153 | if ($enum) { 154 | $phpType = $enum->backedType; 155 | } 156 | /** @var In $inAnnotation */ 157 | $inAnnotation = ApiAnnotation::getProperty($parameterClassName, $fieldName, In::class)?->toAnnotations()[0]; 158 | if (! empty($inAnnotation)) { 159 | $schema->enum = $inAnnotation->getValue(); 160 | } 161 | if (! empty($enum)) { 162 | $schema->enum = $enum->valueList; 163 | } 164 | } 165 | 166 | $schema->type = $this->common->getSwaggerType($phpType); 167 | 168 | /** @var Required $requiredAnnotation */ 169 | $requiredAnnotation = ApiAnnotation::getProperty($parameterClassName, $fieldName, Required::class)?->toAnnotations()[0]; 170 | if ($apiModelProperty->required || $requiredAnnotation) { 171 | $parameter->required = true; 172 | } 173 | $parameter->schema = $schema; 174 | $parameter->description = $apiModelProperty->value ?? ''; 175 | $parameters[] = $parameter; 176 | } 177 | return $parameters; 178 | } 179 | 180 | protected function getContent(string $className, string $mediaTypeStr = 'application/json'): array 181 | { 182 | $arr = []; 183 | $mediaType = new OA\MediaType(); 184 | $mediaType->mediaType = $mediaTypeStr; 185 | $mediaType->schema = $this->getJsonContent($className); 186 | $arr[] = $mediaType; 187 | return $arr; 188 | } 189 | 190 | protected function getJsonContent(string $className): OA\JsonContent 191 | { 192 | $jsonContent = new OA\JsonContent(); 193 | $this->swaggerComponents->generateSchemas($className); 194 | $jsonContent->ref = $this->common->getComponentsName($className); 195 | return $jsonContent; 196 | } 197 | 198 | /** 199 | * @param BaseParam[] $baseParam 200 | */ 201 | protected function getParameterArrByBaseParam(array $baseParam): array 202 | { 203 | $parameters = []; 204 | foreach ($baseParam as $param) { 205 | if ($param->hidden) { 206 | continue; 207 | } 208 | $parameter = new OA\Parameter(); 209 | $schema = new OA\Schema(); 210 | $parameter->name = $param->name; 211 | $parameter->in = $param->getIn(); 212 | $schema->default = $param->default; 213 | $schema->type = $this->common->getSwaggerType($param->type); 214 | // 描述 215 | $parameter->description = $param->description; 216 | if ($param->required !== null) { 217 | $parameter->required = $param->required; 218 | } 219 | $schema->default = $param->default; 220 | $schema->format = $param->format; 221 | $parameter->schema = $schema; 222 | $parameters[] = $parameter; 223 | } 224 | return $parameters; 225 | } 226 | 227 | /** 228 | * @param BaseParam[] $baseParam 229 | */ 230 | protected function getPropertiesByBaseParam(array $baseParam): array 231 | { 232 | $propertyArr = []; 233 | $requiredArr = []; 234 | 235 | foreach ($baseParam as $param) { 236 | if ($param->hidden) { 237 | continue; 238 | } 239 | // 属性 240 | $property = new OA\Property(); 241 | // 字段名称 242 | $fieldName = $param->name; 243 | $property->property = $fieldName; 244 | // 描述 245 | $property->description = $param->description; 246 | $param->required && $requiredArr[] = $fieldName; 247 | $property->default = $param->default; 248 | $property->type = $this->common->getSwaggerType($param->type); 249 | $property->format = $param->format; 250 | $propertyArr[] = $property; 251 | } 252 | return ['propertyArr' => $propertyArr, 'requiredArr' => $requiredArr]; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Hyperf API Docs 2 | 3 | [![Latest Stable Version](https://img.shields.io/packagist/v/tangwei/apidocs)](https://packagist.org/packages/tangwei/apidocs) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/tangwei/apidocs)](https://packagist.org/packages/tangwei/apidocs) 5 | [![License](https://img.shields.io/packagist/l/tangwei/apidocs)](https://github.com/tw2066/api-docs) 6 | [![PHP Version](https://img.shields.io/badge/php-%3E%3D8.1-blue)](https://www.php.net) 7 | 8 | [English](./README_EN.md) | 中文 9 | 10 | 基于 [Hyperf](https://github.com/hyperf/hyperf) 框架的 Swagger/OpenAPI 文档自动生成组件,支持 Swoole/Swow 引擎,为您提供优雅的 API 文档解决方案。 11 | 12 | ## ✨ 特性 13 | 14 | - 🚀 **自动生成** - 基于 PHP 8 Attributes 自动生成 OpenAPI 3.0/3.1 文档 15 | - 🎯 **类型安全** - 支持 DTO 模式,参数自动映射到 PHP 类 16 | - 📝 **多种 UI** - 支持 Swagger UI、Knife4j、Redoc、RapiDoc、Scalar 等多种文档界面 17 | - ✅ **数据验证** - 集成 Hyperf 验证器,支持丰富的验证注解 18 | - 🔒 **安全认证** - 支持 API Token 和多种安全方案 19 | - 🔄 **类型支持** - 支持数组、递归、嵌套、枚举等复杂类型 20 | - 🎨 **灵活配置** - 可自定义全局响应格式、路由前缀等 21 | - 📦 **开箱即用** - 零配置即可使用,同时支持深度定制 22 | 23 | ## 📋 环境要求 24 | 25 | - PHP >= 8.1 26 | - Hyperf >= 3.0 27 | - Swoole >= 5.0 或 Swow 28 | 29 | ## 💡 使用须知 30 | 31 | - 控制器方法尽可能返回具体的类(包含简单类型),这样能更好地生成文档 32 | - 当返回类无法满足需求时,可使用 `#[ApiResponse]` 注解补充 33 | 34 | ## 📦 安装 35 | 36 | ```bash 37 | composer require tangwei/apidocs 38 | ``` 39 | 40 | 默认使用 Swagger UI,推荐安装 Knife4j UI(可选): 41 | 42 | ```bash 43 | composer require tangwei/knife4j-ui 44 | ``` 45 | 46 | ## 🚀 快速开始 47 | 48 | ### 1. 发布配置文件 49 | 50 | ```bash 51 | php bin/hyperf.php vendor:publish tangwei/apidocs 52 | ``` 53 | 54 | 配置文件将发布到 `config/autoload/api_docs.php` 55 | 56 | ### 2. 基础配置 57 | 58 | ```php 59 | env('APP_ENV') !== 'prod', 64 | 65 | // 文档访问路径 66 | 'prefix_url' => env('API_DOCS_PREFIX_URL', '/swagger'), 67 | 68 | // 基础信息 69 | 'swagger' => [ 70 | 'info' => [ 71 | 'title' => 'API 文档', 72 | 'version' => '1.0.0', 73 | 'description' => '项目 API 文档', 74 | ], 75 | 'servers' => [ 76 | [ 77 | 'url' => 'http://127.0.0.1:9501', 78 | 'description' => 'API 服务器', 79 | ], 80 | ], 81 | ], 82 | ]; 83 | ``` 84 | 85 | > 完整配置文件示例:config/autoload/api_docs.php 86 | 87 |
88 | 完整配置说明(点击展开) 89 |

90 | 91 | ```php 92 | use Hyperf\ApiDocs\DTO\GlobalResponse; 93 | use function Hyperf\Support\env; 94 | 95 | return [ 96 | /* 97 | |-------------------------------------------------------------------------- 98 | | 启动 swagger 服务 99 | |-------------------------------------------------------------------------- 100 | | 101 | | false 将不会启动 swagger 服务 102 | | 103 | */ 104 | 'enable' => env('APP_ENV') !== 'prod', 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | 生成swagger文件格式 109 | |-------------------------------------------------------------------------- 110 | | 111 | | 支持json和yaml 112 | | 113 | */ 114 | 'format' => 'json', 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | 生成swagger文件路径 119 | |-------------------------------------------------------------------------- 120 | */ 121 | 'output_dir' => BASE_PATH . '/runtime/container', 122 | 123 | /* 124 | |-------------------------------------------------------------------------- 125 | | 生成代理类路径 126 | |-------------------------------------------------------------------------- 127 | */ 128 | 'proxy_dir' => BASE_PATH . '/runtime/container/proxy', 129 | 130 | /* 131 | |-------------------------------------------------------------------------- 132 | | 设置路由前缀 133 | |-------------------------------------------------------------------------- 134 | */ 135 | 'prefix_url' => env('API_DOCS_PREFIX_URL', '/swagger'), 136 | 137 | /* 138 | |-------------------------------------------------------------------------- 139 | | 设置swagger资源路径,cdn资源 140 | |-------------------------------------------------------------------------- 141 | */ 142 | 'prefix_swagger_resources' => 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.5.0', 143 | 144 | /* 145 | |-------------------------------------------------------------------------- 146 | | 设置全局返回的代理类 147 | |-------------------------------------------------------------------------- 148 | | 149 | | 全局返回 如:[code=>200,data=>null] 格式,设置会后会全局生成对应文档 150 | | 配合ApiVariable注解使用,示例参考GlobalResponse类 151 | | 返回数据格式可以利用AOP统一返回 152 | | 153 | */ 154 | // 'global_return_responses_class' => GlobalResponse::class, 155 | 156 | /* 157 | |-------------------------------------------------------------------------- 158 | | 替换验证属性 159 | |-------------------------------------------------------------------------- 160 | | 161 | | 通过获取注解ApiModelProperty的值,来提供数据验证的提示信息 162 | | 163 | */ 164 | 'validation_custom_attributes' => true, 165 | 166 | /* 167 | |-------------------------------------------------------------------------- 168 | | 设置DTO类默认值等级 169 | |-------------------------------------------------------------------------- 170 | | 171 | | 设置:0 默认(不设置默认值) 172 | | 设置:1 简单类型会为设置默认值,复杂类型(带?)会设置null 173 | | - 简单类型默认值: int:0 float:0 string:'' bool:false array:[] mixed:null 174 | | 设置:2 (慎用)包含等级1且复杂类型(联合类型除外)会设置null 175 | | 176 | */ 177 | 'dto_default_value_level' => 0, 178 | 179 | /* 180 | |-------------------------------------------------------------------------- 181 | | 全局responses,映射到ApiResponse注解对象 182 | |-------------------------------------------------------------------------- 183 | */ 184 | 'responses' => [ 185 | ['response' => 401, 'description' => 'Unauthorized'], 186 | ['response' => 500, 'description' => 'System error'], 187 | ], 188 | /* 189 | |-------------------------------------------------------------------------- 190 | | swagger 的基础配置 191 | |-------------------------------------------------------------------------- 192 | | 193 | | 该属性会映射到OpenAPI对象 194 | | 195 | */ 196 | 'swagger' => [ 197 | 'info' => [ 198 | 'title' => 'API DOC', 199 | 'version' => '0.1', 200 | 'description' => 'swagger api desc', 201 | ], 202 | 'servers' => [ 203 | [ 204 | 'url' => 'http://127.0.0.1:9501', 205 | 'description' => 'OpenApi host', 206 | ], 207 | ], 208 | 'components' => [ 209 | 'securitySchemes' => [ 210 | [ 211 | 'securityScheme' => 'Authorization', 212 | 'type' => 'apiKey', 213 | 'in' => 'header', 214 | 'name' => 'Authorization', 215 | ], 216 | ], 217 | ], 218 | 'security' => [ 219 | ['Authorization' => []], 220 | ], 221 | 'externalDocs' => [ 222 | 'description' => 'Find out more about Swagger', 223 | 'url' => 'https://github.com/tw2066/api-docs', 224 | ], 225 | ], 226 | ]; 227 | ``` 228 |

229 |
230 | 231 | ### 3. 启动服务 232 | 233 | ```bash 234 | php bin/hyperf.php start 235 | ``` 236 | 237 | 启动成功后,访问 `http://your-host:9501/swagger` 即可查看 API 文档。 238 | 239 | ``` 240 | [INFO] Swagger docs url at http://0.0.0.0:9501/swagger 241 | [INFO] Worker#0 started. 242 | [INFO] HTTP Server listening at 0.0.0.0:9501 243 | ``` 244 | 245 | ## 📖 使用指南 246 | 247 | ### 基础示例 248 | 249 | #### 1. 定义 DTO 类 250 | 251 | ```php 252 | 1, 'username' => 'admin'], 305 | ['id' => 2, 'username' => 'user'], 306 | ]; 307 | } 308 | 309 | #[PostMapping(path: 'create')] 310 | #[ApiOperation(summary: '创建用户')] 311 | public function create(#[RequestBody] #[Valid] UserRequest $request): array 312 | { 313 | return [ 314 | 'id' => 1, 315 | 'username' => $request->username, 316 | 'age' => $request->age, 317 | ]; 318 | } 319 | } 320 | ``` 321 | 322 | ## 🎨 注解参考 323 | 324 | ### 控制器注解 325 | 326 | #### `#[Api]` - 控制器标签 327 | 328 | ```php 329 | #[Api( 330 | tags: '用户管理', // 标签名称(支持数组) 331 | description: '用户相关操作', // 描述 332 | position: 1, // 排序位置 333 | hidden: false // 是否隐藏 334 | )] 335 | ``` 336 | 337 | #### `#[ApiOperation]` - API 操作 338 | 339 | ```php 340 | #[ApiOperation( 341 | summary: '创建用户', // 摘要 342 | description: '详细描述', // 详细描述 343 | deprecated: false, // 是否废弃 344 | security: true, // 是否需要认证 345 | hidden: false // 是否隐藏 346 | )] 347 | ``` 348 | 349 | #### `#[ApiResponse]` - 响应定义 350 | 351 | ```php 352 | // 简单类型响应 353 | #[ApiResponse(PhpType::INT, 200, '成功')] 354 | 355 | // 对象响应 356 | #[ApiResponse(UserResponse::class, 200, '用户信息')] 357 | 358 | // 数组响应 359 | #[ApiResponse([UserResponse::class], 200, '用户列表')] 360 | 361 | // 分页响应 362 | #[ApiResponse(new Page([UserResponse::class]), 200, '分页数据')] 363 | ``` 364 | 365 | **泛型支持示例:** 366 | 367 | PHP 暂不支持泛型,可通过 `#[ApiVariable]` 实现: 368 | 369 | ```php 370 | use Hyperf\ApiDocs\Annotation\ApiVariable; 371 | 372 | class Page 373 | { 374 | public int $total; 375 | 376 | #[ApiVariable] 377 | public array $content; 378 | 379 | public function __construct(array $content, int $total = 0) 380 | { 381 | $this->content = $content; 382 | $this->total = $total; 383 | } 384 | } 385 | ``` 386 | 387 | 控制器使用: 388 | 389 | ```php 390 | #[ApiOperation('分页查询')] 391 | #[GetMapping(path: 'page')] 392 | #[ApiResponse(new Page([UserResponse::class]))] 393 | public function page(#[RequestQuery] PageQuery $query): Page 394 | { 395 | // 返回分页数据 396 | } 397 | ``` 398 | 399 | ### 参数注解 400 | 401 | #### `#[RequestBody]` - Body 参数 402 | 403 | 获取 POST/PUT/PATCH 请求的 JSON body 参数: 404 | 405 | ```php 406 | public function create(#[RequestBody] #[Valid] UserRequest $request) 407 | { 408 | // $request 自动填充 body 数据 409 | } 410 | ``` 411 | 412 | #### `#[RequestQuery]` - Query 参数 413 | 414 | 获取 URL 查询参数(GET 参数): 415 | 416 | ```php 417 | public function list(#[RequestQuery] #[Valid] QueryRequest $request) 418 | { 419 | // $request 自动填充查询参数 420 | } 421 | ``` 422 | 423 | #### `#[RequestFormData]` - 表单参数 424 | 425 | 获取表单数据(multipart/form-data): 426 | 427 | ```php 428 | #[ApiFormData(name: 'photo', format: 'binary')] 429 | public function upload(#[RequestFormData] UploadRequest $formData) 430 | { 431 | $file = $this->request->file('photo'); 432 | // 处理文件上传 433 | } 434 | ``` 435 | 436 | #### `#[RequestHeader]` - 请求头参数 437 | 438 | 获取请求头信息: 439 | 440 | ```php 441 | public function auth(#[RequestHeader] #[Valid] AuthHeader $header) 442 | { 443 | // $header 自动填充请求头数据 444 | } 445 | ``` 446 | 447 | > ⚠️ **注意**:一个方法不能同时注入 `RequestBody` 和 `RequestFormData` 448 | 449 | ### 属性注解 450 | 451 | #### `#[ApiModelProperty]` - 属性描述 452 | 453 | ```php 454 | #[ApiModelProperty( 455 | value: '用户名', // 属性描述 456 | example: 'admin', // 示例值 457 | required: true, // 是否必填 458 | hidden: false // 是否隐藏 459 | )] 460 | public string $username; 461 | ``` 462 | 463 | #### `#[ApiHeader]` - 请求头定义 464 | 465 | ```php 466 | // 全局请求头(类级别) 467 | #[ApiHeader('X-Request-Id')] 468 | 469 | // 方法级请求头 470 | #[ApiHeader( 471 | name: 'Authorization', 472 | required: true, 473 | type: 'string', 474 | description: 'Bearer token' 475 | )] 476 | ``` 477 | 478 | #### `#[ApiSecurity]` - 安全认证 479 | 480 | 优先级:方法 > 类 > 全局 481 | 482 | ```php 483 | // 使用默认认证 484 | #[ApiSecurity('Authorization')] 485 | 486 | // 方法级覆盖 487 | #[ApiOperation(summary: '登录', security: false)] // 不需要认证 488 | ``` 489 | 490 | 491 | 492 | ## ✅ 数据验证 493 | 494 | ### 内置验证注解 495 | 496 | 组件提供丰富的验证注解支持: 497 | 498 | ```php 499 | use Hyperf\DTO\Annotation\Validation\*; 500 | 501 | class UserRequest 502 | { 503 | #[Required] // 必填 504 | #[Max(50)] // 最大长度 505 | public string $username; 506 | 507 | #[Required] 508 | #[Integer] // 整数 509 | #[Between(1, 120)] // 范围 510 | public int $age; 511 | 512 | #[Email] // 邮箱格式 513 | public ?string $email; 514 | 515 | #[Url] // URL 格式 516 | public ?string $website; 517 | 518 | #[Regex('/^1[3-9]\d{9}$/')] // 正则验证 519 | public ?string $mobile; 520 | 521 | #[In(['male', 'female'])] // 枚举值 522 | public ?string $gender; 523 | 524 | #[Date] // 日期格式 525 | public ?string $birthday; 526 | } 527 | ``` 528 | 529 | > 💡 **提示**:只需在控制器方法参数中添加 `#[Valid]` 注解即可启用验证 530 | 531 | ```php 532 | public function create(#[RequestBody] #[Valid] UserRequest $request) 533 | { 534 | // 验证自动执行 535 | } 536 | ``` 537 | 538 | ### 自定义验证 539 | 540 | #### 使用 Validation 注解 541 | 542 | ```php 543 | // 支持 Laravel 风格的验证规则 544 | #[Validation('required|string|min:3|max:50')] 545 | public string $username; 546 | 547 | // 数组元素验证 548 | #[Validation('integer', customKey: 'ids.*')] 549 | public array $ids; 550 | ``` 551 | 552 | #### 自定义验证注解 553 | 554 | ```php 555 | \App\DTO\GlobalResponse::class, 674 | ]; 675 | ``` 676 | 677 | 定义全局响应类: 678 | 679 | ```php 680 | request->file('file'); 711 | // 处理文件上传 712 | return ['url' => '/uploads/file.jpg']; 713 | } 714 | ``` 715 | 716 | ## 🎭 多种 UI 界面 717 | 718 | 访问不同的 UI 界面: 719 | 720 | - **Swagger UI**: `http://your-host:9501/swagger` 721 | - **Knife4j**: `http://your-host:9501/swagger/knife4j` 722 | - **Redoc**: `http://your-host:9501/swagger/redoc` 723 | - **RapiDoc**: `http://your-host:9501/swagger/rapidoc` 724 | - **Scalar**: `http://your-host:9501/swagger/scalar` 725 | 726 | ## ⚙️ 配置参考 727 | 728 | ### DTO 数据映射 729 | 730 | > api-docs 依赖 DTO 组件,更多详情请查看 [DTO 文档](https://github.com/hyperf/dto) 731 | 732 | #### `#[Dto]` 注解 733 | 734 | 标记为 DTO 类: 735 | 736 | ```php 737 | use Hyperf\DTO\Annotation\Dto; 738 | 739 | #[Dto] 740 | class DemoQuery 741 | { 742 | } 743 | ``` 744 | 745 | - 可以设置返回格式 `#[Dto(Convert::SNAKE)]`,批量转换为下划线格式的 key 746 | - `Dto` 注解不会生成文档,要生成对应文档使用 `JSONField` 注解 747 | 748 | #### `#[JSONField]` 注解 749 | 750 | 用于设置属性的别名: 751 | 752 | ```php 753 | use Hyperf\DTO\Annotation\Dto; 754 | use Hyperf\DTO\Annotation\JSONField; 755 | 756 | #[Dto] 757 | class DemoQuery 758 | { 759 | #[ApiModelProperty('这是一个别名')] 760 | #[JSONField('alias_name')] 761 | #[Required] 762 | public string $name; 763 | } 764 | ``` 765 | 766 | - 设置 `JSONField` 后会生成代理类,生成 `alias_name` 属性 767 | - 接收和返回字段都以 `alias_name` 为准 768 | 769 | ### RPC 支持 770 | 771 | [返回 PHP 对象](https://hyperf.wiki/3.1/#/zh-cn/json-rpc?id=%e8%bf%94%e5%9b%9e-php-%e5%af%b9%e8%b1%a1) 772 | 773 | aspects.php 中配置: 774 | 775 | ```php 776 | return [ 777 | \Hyperf\DTO\Aspect\ObjectNormalizerAspect::class 778 | ] 779 | ``` 780 | 781 | 当框架导入 `symfony/serializer (^5.0)` 和 `symfony/property-access (^5.0)` 后,在 dependencies.php 中配置映射关系: 782 | 783 | ```php 784 | use Hyperf\Serializer\SerializerFactory; 785 | use Hyperf\Serializer\Serializer; 786 | 787 | return [ 788 | Hyperf\Contract\NormalizerInterface::class => new SerializerFactory(Serializer::class), 789 | ]; 790 | ``` 791 | 792 | ## 💡 最佳实践 793 | 794 | ### 1. DTO 类设计 795 | 796 | - 使用有意义的类名,如 `CreateUserRequest`、`UserResponse` 797 | - 为每个属性添加 `ApiModelProperty` 注解 798 | - 分离 Request 和 Response 定义 799 | - 合理使用验证注解 800 | 801 | ### 2. 控制器设计 802 | 803 | - 使用 `Api` 注解对控制器分组 804 | - 为每个方法添加 `ApiOperation` 描述 805 | - 尽可能返回具体类型而非 `array` 806 | - 合理使用 `ApiResponse` 定义响应格式 807 | 808 | ### 3. 安全性 809 | 810 | - 生产环境禁用文档服务 811 | - 使用 `ApiSecurity` 控制 API 认证 812 | - 使用 `hidden: true` 隐藏敏感接口 813 | 814 | ### 4. 性能优化 815 | 816 | - 开发环境使用文档,生产环境禁用 817 | - 合理使用缓存 818 | - 避免过深的嵌套结构 819 | 820 | ## 📚 常见问题 821 | 822 | ### Q: 文档没有生成? 823 | 824 | A: 检查以下几点: 825 | 1. 配置文件中 `enable` 是否为 `true` 826 | 2. 查看日志是否有错误信息 827 | 828 | ### Q: 如何定义数组类型? 829 | 830 | A: 使用 PHPDoc 注释或 `ArrayType` 注解: 831 | 832 | ```php 833 | /** 834 | * @var User[] 835 | */ 836 | public array $users; 837 | 838 | // 或 839 | #[ArrayType(User::class)] 840 | public array $users; 841 | ``` 842 | 843 | ### Q: 如何隐藏某些接口? 844 | 845 | A: 使用 `hidden` 参数: 846 | 847 | ```php 848 | #[Api(hidden: true)] // 隐藏整个控制器 849 | 850 | #[ApiOperation(summary: '测试', hidden: true)] // 隐藏单个接口 851 | ``` 852 | 853 | ### Q: 如何自定义响应格式? 854 | 855 | A: 使用 `ApiResponse` 注解或配置全局响应类: 856 | 857 | ```php 858 | #[ApiResponse(UserResponse::class, 200, '成功')] 859 | public function getUser(): UserResponse 860 | { 861 | return new UserResponse(); 862 | } 863 | ``` 864 | 865 | ### Q: 支持哪些验证规则? 866 | 867 | A: 支持所有 Hyperf Validation 规则。详见 [Hyperf 验证器文档](https://hyperf.wiki/3.1/#/zh-cn/validation)。 868 | 869 | ### Q: `AutoController` 注解支持吗? 870 | 871 | A: 支持,但只会收集 `POST` 方法。建议使用标准路由注解以获得更好的文档生成效果。 872 | 873 | ## 📖 示例项目 874 | 875 | > 完整示例请参考 [example 目录](https://github.com/tw2066/api-docs/tree/master/example) 876 | 877 | ## 🔗 相关链接 878 | 879 | - [Hyperf 官方文档](https://hyperf.wiki) 880 | - [OpenAPI 规范](https://swagger.io/specification/) 881 | - [Swagger UI](https://swagger.io/tools/swagger-ui/) 882 | - [Knife4j](https://doc.xiaominfo.com/) 883 | - [示例项目](https://github.com/tw2066/api-docs/tree/master/example) 884 | 885 | --- 886 | 887 | 如果这个项目对你有帮助,请给个 ⭐ Star! 888 | 889 | 890 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # Hyperf API Docs 2 | 3 | [![Latest Stable Version](https://img.shields.io/packagist/v/tangwei/apidocs)](https://packagist.org/packages/tangwei/apidocs) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/tangwei/apidocs)](https://packagist.org/packages/tangwei/apidocs) 5 | [![License](https://img.shields.io/packagist/l/tangwei/apidocs)](https://github.com/tw2066/api-docs) 6 | [![PHP Version](https://img.shields.io/badge/php-%3E%3D8.1-blue)](https://www.php.net) 7 | 8 | English | [中文](./README.md) 9 | 10 | Automatic Swagger/OpenAPI documentation generator for the [Hyperf](https://github.com/hyperf/hyperf) framework, supporting Swoole/Swow engines, providing an elegant and powerful API documentation solution. 11 | 12 | ## ✨ Features 13 | 14 | - 🚀 **Auto Generation** - Automatically generate OpenAPI 3.0 documentation based on PHP 8 Attributes 15 | - 🎯 **Type Safety** - Support DTO mode with automatic parameter mapping to PHP classes 16 | - 📝 **Multiple UIs** - Support Swagger UI, Knife4j, Redoc, RapiDoc, Scalar, and more 17 | - ✅ **Data Validation** - Integrate Hyperf validator with rich validation annotations 18 | - 🔒 **Security** - Support API Token and multiple security schemes 19 | - 🔄 **Type Support** - Support arrays, recursion, nesting, enums, and other complex types 20 | - 🎨 **Flexible Config** - Customizable global response format, route prefix, etc. 21 | - 📦 **Out of Box** - Zero configuration ready to use with deep customization support 22 | 23 | ## 📋 Requirements 24 | 25 | - PHP >= 8.1 26 | - Hyperf >= 3.0 27 | - Swoole >= 5.0 or Swow 28 | 29 | ## 💡 Important Notes 30 | 31 | - Union types are not supported for parameter mapping to PHP classes 32 | - Controller methods should return specific types (including simple types) for better documentation generation 33 | - Use `#[ApiResponse]` annotation when return types cannot fully express the response structure 34 | 35 | ## 📦 Installation 36 | 37 | ```bash 38 | composer require tangwei/apidocs 39 | ``` 40 | 41 | By default, Swagger UI is used. You can optionally install Knife4j UI (recommended): 42 | 43 | ```bash 44 | composer require tangwei/knife4j-ui 45 | ``` 46 | 47 | ## 🚀 Quick Start 48 | 49 | ### 1. Publish Configuration 50 | 51 | ```bash 52 | php bin/hyperf.php vendor:publish tangwei/apidocs 53 | ``` 54 | 55 | Configuration file will be published to `config/autoload/api_docs.php` 56 | 57 |
58 | Complete Configuration Reference (Click to expand) 59 |

60 | 61 | > Full configuration example: config/autoload/api_docs.php 62 | 63 | ```php 64 | env('APP_ENV') !== 'prod', 78 | 79 | /* 80 | |-------------------------------------------------------------------------- 81 | | Swagger File Format 82 | |-------------------------------------------------------------------------- 83 | | 84 | | Supports json and yaml 85 | | 86 | */ 87 | 'format' => 'json', 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Swagger File Output Path 92 | |-------------------------------------------------------------------------- 93 | */ 94 | 'output_dir' => BASE_PATH . '/runtime/container', 95 | 96 | /* 97 | |-------------------------------------------------------------------------- 98 | | Proxy Class Path 99 | |-------------------------------------------------------------------------- 100 | */ 101 | 'proxy_dir' => BASE_PATH . '/runtime/container/proxy', 102 | 103 | /* 104 | |-------------------------------------------------------------------------- 105 | | Route Prefix 106 | |-------------------------------------------------------------------------- 107 | */ 108 | 'prefix_url' => env('API_DOCS_PREFIX_URL', '/swagger'), 109 | 110 | /* 111 | |-------------------------------------------------------------------------- 112 | | Swagger Resources CDN Path 113 | |-------------------------------------------------------------------------- 114 | */ 115 | 'prefix_swagger_resources' => 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.27.1', 116 | 117 | /* 118 | |-------------------------------------------------------------------------- 119 | | Global Response Class 120 | |-------------------------------------------------------------------------- 121 | | 122 | | Global response format like: [code=>200, data=>null] 123 | | Use with ApiVariable annotation, see GlobalResponse class example 124 | | Response format can be unified using AOP 125 | | 126 | */ 127 | // 'global_return_responses_class' => GlobalResponse::class, 128 | 129 | /* 130 | |-------------------------------------------------------------------------- 131 | | Replace Validation Attributes 132 | |-------------------------------------------------------------------------- 133 | | 134 | | Use ApiModelProperty annotation values for validation error messages 135 | | 136 | */ 137 | 'validation_custom_attributes' => true, 138 | 139 | /* 140 | |-------------------------------------------------------------------------- 141 | | DTO Default Value Level 142 | |-------------------------------------------------------------------------- 143 | | 144 | | 0: Default (no default values) 145 | | 1: Simple types get default values, complex types with ? get null 146 | | - Simple type defaults: int:0 float:0 string:'' bool:false array:[] mixed:null 147 | | 2: (Use with caution) Includes level 1 and complex types (except union) get null 148 | | 149 | */ 150 | 'dto_default_value_level' => 0, 151 | 152 | /* 153 | |-------------------------------------------------------------------------- 154 | | Global Responses 155 | |-------------------------------------------------------------------------- 156 | */ 157 | 'responses' => [ 158 | ['response' => 401, 'description' => 'Unauthorized'], 159 | ['response' => 500, 'description' => 'System error'], 160 | ], 161 | 162 | /* 163 | |-------------------------------------------------------------------------- 164 | | Swagger Basic Configuration 165 | |-------------------------------------------------------------------------- 166 | | 167 | | This maps to OpenAPI object 168 | | 169 | */ 170 | 'swagger' => [ 171 | 'info' => [ 172 | 'title' => 'API Documentation', 173 | 'version' => '1.0.0', 174 | 'description' => 'API Documentation', 175 | ], 176 | 'servers' => [ 177 | [ 178 | 'url' => 'http://127.0.0.1:9501', 179 | 'description' => 'API Server', 180 | ], 181 | ], 182 | 'components' => [ 183 | 'securitySchemes' => [ 184 | [ 185 | 'securityScheme' => 'Authorization', 186 | 'type' => 'apiKey', 187 | 'in' => 'header', 188 | 'name' => 'Authorization', 189 | ], 190 | ], 191 | ], 192 | 'security' => [ 193 | ['Authorization' => []], 194 | ], 195 | 'externalDocs' => [ 196 | 'description' => 'GitHub', 197 | 'url' => 'https://github.com/tw2066/api-docs', 198 | ], 199 | ], 200 | ]; 201 | ``` 202 |

203 |
204 | 205 | ### 2. Basic Configuration 206 | 207 | ```php 208 | env('APP_ENV') !== 'prod', 213 | 214 | // Documentation access path 215 | 'prefix_url' => env('API_DOCS_PREFIX_URL', '/swagger'), 216 | 217 | // Basic information 218 | 'swagger' => [ 219 | 'info' => [ 220 | 'title' => 'API Documentation', 221 | 'version' => '1.0.0', 222 | 'description' => 'Project API Documentation', 223 | ], 224 | 'servers' => [ 225 | [ 226 | 'url' => 'http://127.0.0.1:9501', 227 | 'description' => 'API Server', 228 | ], 229 | ], 230 | ], 231 | ]; 232 | ``` 233 | 234 | ### 3. Start Server 235 | 236 | ```bash 237 | php bin/hyperf.php start 238 | ``` 239 | 240 | After successful startup, visit `http://your-host:9501/swagger` to view the API documentation. 241 | 242 | ``` 243 | [INFO] Swagger docs url at http://0.0.0.0:9501/swagger 244 | [INFO] Worker#0 started. 245 | [INFO] HTTP Server listening at 0.0.0.0:9501 246 | ``` 247 | 248 | ## 📖 Usage Guide 249 | 250 | ### Basic Example 251 | 252 | #### 1. Define DTO Class 253 | 254 | ```php 255 | 1, 'username' => 'admin'], 308 | ['id' => 2, 'username' => 'user'], 309 | ]; 310 | } 311 | 312 | #[PostMapping(path: 'create')] 313 | #[ApiOperation(summary: 'Create user')] 314 | public function create(#[RequestBody] #[Valid] UserRequest $request): array 315 | { 316 | return [ 317 | 'id' => 1, 318 | 'username' => $request->username, 319 | 'age' => $request->age, 320 | ]; 321 | } 322 | } 323 | ``` 324 | 325 | ## 🎨 Annotation Reference 326 | 327 | ### Controller Annotations 328 | 329 | #### `#[Api]` - Controller Tag 330 | 331 | ```php 332 | #[Api( 333 | tags: 'User Management', // Tag name (supports array) 334 | description: 'User operations', // Description 335 | position: 1, // Sort position 336 | hidden: false // Whether to hide 337 | )] 338 | ``` 339 | 340 | #### `#[ApiOperation]` - API Operation 341 | 342 | ```php 343 | #[ApiOperation( 344 | summary: 'Create user', // Summary 345 | description: 'Detailed description', // Detailed description 346 | deprecated: false, // Whether deprecated 347 | security: true, // Whether authentication required 348 | hidden: false // Whether to hide 349 | )] 350 | ``` 351 | 352 | #### `#[ApiResponse]` - Response Definition 353 | 354 | ```php 355 | // Simple type response 356 | #[ApiResponse(PhpType::INT, 200, 'Success')] 357 | 358 | // Object response 359 | #[ApiResponse(UserResponse::class, 200, 'User information')] 360 | 361 | // Array response 362 | #[ApiResponse([UserResponse::class], 200, 'User list')] 363 | 364 | // Paginated response 365 | #[ApiResponse(new Page([UserResponse::class]), 200, 'Paginated data')] 366 | ``` 367 | 368 | ### Parameter Annotations 369 | 370 | #### `#[RequestBody]` - Body Parameters 371 | 372 | Get JSON body parameters from POST/PUT/PATCH requests: 373 | 374 | ```php 375 | public function create(#[RequestBody] #[Valid] UserRequest $request) 376 | { 377 | // $request automatically populated with body data 378 | } 379 | ``` 380 | 381 | #### `#[RequestQuery]` - Query Parameters 382 | 383 | Get URL query parameters (GET parameters): 384 | 385 | ```php 386 | public function list(#[RequestQuery] #[Valid] QueryRequest $request) 387 | { 388 | // $request automatically populated with query parameters 389 | } 390 | ``` 391 | 392 | #### `#[RequestFormData]` - Form Parameters 393 | 394 | Get form data (multipart/form-data): 395 | 396 | ```php 397 | #[ApiFormData(name: 'photo', format: 'binary')] 398 | public function upload(#[RequestFormData] UploadRequest $formData) 399 | { 400 | $file = $this->request->file('photo'); 401 | // Handle file upload 402 | } 403 | ``` 404 | 405 | #### `#[RequestHeader]` - Header Parameters 406 | 407 | Get request header information: 408 | 409 | ```php 410 | public function auth(#[RequestHeader] #[Valid] AuthHeader $header) 411 | { 412 | // $header automatically populated with header data 413 | } 414 | ``` 415 | 416 | **Generic Type Support Example:** 417 | 418 | PHP doesn't natively support generics, but you can achieve similar functionality using `#[ApiVariable]`: 419 | 420 | ```php 421 | use Hyperf\ApiDocs\Annotation\ApiVariable; 422 | 423 | class Page 424 | { 425 | public int $total; 426 | 427 | #[ApiVariable] 428 | public array $content; 429 | 430 | public function __construct(array $content, int $total = 0) 431 | { 432 | $this->content = $content; 433 | $this->total = $total; 434 | } 435 | } 436 | ``` 437 | 438 | Controller usage: 439 | 440 | ```php 441 | #[ApiOperation('Paginated query')] 442 | #[GetMapping(path: 'page')] 443 | #[ApiResponse(new Page([UserResponse::class]))] 444 | public function page(#[RequestQuery] PageQuery $query): Page 445 | { 446 | // Return paginated data 447 | } 448 | ``` 449 | 450 | ### Property Annotations 451 | 452 | #### `#[ApiModelProperty]` - Property Description 453 | 454 | ```php 455 | #[ApiModelProperty( 456 | value: 'Username', // Property description 457 | example: 'admin', // Example value 458 | required: true, // Whether required 459 | hidden: false // Whether to hide 460 | )] 461 | public string $username; 462 | ``` 463 | 464 | #### `#[ApiHeader]` - Header Definition 465 | 466 | ```php 467 | // Global header (class level) 468 | #[ApiHeader('X-Request-Id')] 469 | 470 | // Method level header 471 | #[ApiHeader( 472 | name: 'Authorization', 473 | required: true, 474 | type: 'string', 475 | description: 'Bearer token' 476 | )] 477 | ``` 478 | 479 | #### `#[ApiSecurity]` - Security Authentication 480 | 481 | Priority: Method > Class > Global 482 | 483 | ```php 484 | // Use default authentication 485 | #[ApiSecurity('Authorization')] 486 | 487 | // Method level override 488 | #[ApiOperation(summary: 'Login', security: false)] // No authentication required 489 | ``` 490 | 491 | > ⚠️ **Note**: A method cannot inject both `RequestBody` and `RequestFormData` simultaneously 492 | 493 | ## ✅ Data Validation 494 | 495 | ### Built-in Validation Annotations 496 | 497 | The component provides rich validation annotations: 498 | 499 | ```php 500 | use Hyperf\DTO\Annotation\Validation\*; 501 | 502 | class UserRequest 503 | { 504 | #[Required] // Required 505 | #[Max(50)] // Max length 506 | public string $username; 507 | 508 | #[Required] 509 | #[Integer] // Integer 510 | #[Between(1, 120)] // Range 511 | public int $age; 512 | 513 | #[Email] // Email format 514 | public ?string $email; 515 | 516 | #[Url] // URL format 517 | public ?string $website; 518 | 519 | #[Regex('/^1[3-9]\d{9}$/')] // Regex validation 520 | public ?string $mobile; 521 | 522 | #[In(['male', 'female'])] // Enum values 523 | public ?string $gender; 524 | 525 | #[Date] // Date format 526 | public ?string $birthday; 527 | } 528 | ``` 529 | 530 | > 💡 **Tip**: Simply add the `#[Valid]` annotation to controller method parameters to enable validation 531 | 532 | ```php 533 | public function create(#[RequestBody] #[Valid] UserRequest $request) 534 | { 535 | // Validation is automatically executed 536 | } 537 | ``` 538 | 539 | ### Custom Validation 540 | 541 | #### Using Validation Annotation 542 | 543 | ```php 544 | // Support Laravel-style validation rules 545 | #[Validation('required|string|min:3|max:50')] 546 | public string $username; 547 | 548 | // Array element validation 549 | #[Validation('integer', customKey: 'ids.*')] 550 | public array $ids; 551 | ``` 552 | 553 | #### Custom Validation Annotation 554 | 555 | ```php 556 | \App\DTO\GlobalResponse::class, 675 | ]; 676 | ``` 677 | 678 | Define global response class: 679 | 680 | ```php 681 | request->file('file'); 712 | // Handle file upload 713 | return ['url' => '/uploads/file.jpg']; 714 | } 715 | ``` 716 | 717 | ## 🔧 Advanced Features 718 | 719 | ### Array Type Support 720 | 721 | #### Method 1: Using PHPDoc 722 | 723 | ```php 724 | /** 725 | * @var Address[] 726 | */ 727 | #[ApiModelProperty('Address list')] 728 | public array $addresses; 729 | 730 | /** 731 | * @var int[] 732 | */ 733 | #[ApiModelProperty('ID list')] 734 | public array $ids; 735 | ``` 736 | 737 | #### Method 2: Using ArrayType Annotation 738 | 739 | ```php 740 | use Hyperf\DTO\Annotation\ArrayType; 741 | 742 | #[ApiModelProperty('Address list')] 743 | #[ArrayType(Address::class)] 744 | public array $addresses; 745 | 746 | #[ApiModelProperty('Tag list')] 747 | #[ArrayType('string')] 748 | public array $tags; 749 | ``` 750 | 751 | ### Nested Objects 752 | 753 | ```php 754 | class UserRequest 755 | { 756 | public string $name; 757 | 758 | // Nested object 759 | #[ApiModelProperty('Address info')] 760 | public Address $address; 761 | 762 | /** 763 | * @var Address[] 764 | */ 765 | #[ApiModelProperty('Multiple addresses')] 766 | public array $addresses; 767 | } 768 | 769 | class Address 770 | { 771 | public string $province; 772 | public string $city; 773 | public string $street; 774 | } 775 | ``` 776 | 777 | ### Enum Support 778 | 779 | ```php 780 | use Hyperf\DTO\Type\PhpType; 781 | 782 | enum StatusEnum: int 783 | { 784 | case PENDING = 0; 785 | case ACTIVE = 1; 786 | case INACTIVE = 2; 787 | } 788 | 789 | class OrderRequest 790 | { 791 | #[ApiModelProperty('Order status')] 792 | public StatusEnum $status; 793 | } 794 | ``` 795 | 796 | ### Global Response Format 797 | 798 | Configure global response wrapper class: 799 | 800 | ```php 801 | // config/autoload/api_docs.php 802 | return [ 803 | 'global_return_responses_class' => \App\DTO\GlobalResponse::class, 804 | ]; 805 | ``` 806 | 807 | Define global response class: 808 | 809 | ```php 810 | request->file('file'); 841 | // Handle file upload 842 | return ['url' => '/uploads/file.jpg']; 843 | } 844 | ``` 845 | 846 | ## 🎭 Multiple UI Interfaces 847 | 848 | Access different UI interfaces: 849 | 850 | - **Swagger UI**: `http://your-host:9501/swagger` 851 | - **Knife4j**: `http://your-host:9501/swagger/knife4j` 852 | - **Redoc**: `http://your-host:9501/swagger/redoc` 853 | - **RapiDoc**: `http://your-host:9501/swagger/rapidoc` 854 | - **Scalar**: `http://your-host:9501/swagger/scalar` 855 | 856 | ## ⚙️ Configuration Reference 857 | 858 | ### DTO Data Mapping 859 | 860 | > api-docs depends on the DTO component. For more details, see [DTO Documentation](https://github.com/hyperf/dto) 861 | 862 | #### `#[Dto]` Annotation 863 | 864 | Mark as DTO class: 865 | 866 | ```php 867 | use Hyperf\DTO\Annotation\Dto; 868 | 869 | #[Dto] 870 | class DemoQuery 871 | { 872 | } 873 | ``` 874 | 875 | - Can set return format `#[Dto(Convert::SNAKE)]` to batch convert keys to snake_case 876 | - `Dto` annotation doesn't generate documentation, use `JSONField` annotation to generate docs 877 | 878 | #### `#[JSONField]` Annotation 879 | 880 | Used to set property aliases: 881 | 882 | ```php 883 | use Hyperf\DTO\Annotation\Dto; 884 | use Hyperf\DTO\Annotation\JSONField; 885 | 886 | #[Dto] 887 | class DemoQuery 888 | { 889 | #[ApiModelProperty('This is an alias')] 890 | #[JSONField('alias_name')] 891 | #[Required] 892 | public string $name; 893 | } 894 | ``` 895 | 896 | - Setting `JSONField` generates proxy class with `alias_name` property 897 | - Both request and response use `alias_name` as the field name 898 | 899 | ### RPC Support 900 | 901 | [Return PHP Object](https://hyperf.wiki/3.1/#/en/json-rpc?id=returning-php-objects) 902 | 903 | Configure in aspects.php: 904 | 905 | ```php 906 | return [ 907 | \Hyperf\DTO\Aspect\ObjectNormalizerAspect::class 908 | ] 909 | ``` 910 | 911 | After importing `symfony/serializer (^5.0)` and `symfony/property-access (^5.0)`, configure mapping in dependencies.php: 912 | 913 | ```php 914 | use Hyperf\Serializer\SerializerFactory; 915 | use Hyperf\Serializer\Serializer; 916 | 917 | return [ 918 | Hyperf\Contract\NormalizerInterface::class => new SerializerFactory(Serializer::class), 919 | ]; 920 | ``` 921 | 922 | ## 💡 Best Practices 923 | 924 | ### 1. DTO Class Design 925 | 926 | - Use meaningful class names like `CreateUserRequest`, `UserResponse` 927 | - Add `ApiModelProperty` annotation for each property 928 | - Separate Request and Response definitions 929 | - Use validation annotations appropriately 930 | 931 | ### 2. Controller Design 932 | 933 | - Use `Api` annotation to group controllers 934 | - Add `ApiOperation` description for each method 935 | - Return specific types instead of `array` when possible 936 | - Use `ApiResponse` to define response formats properly 937 | 938 | ### 3. Security 939 | 940 | - Disable documentation service in production 941 | - Use `ApiSecurity` to control API authentication 942 | - Use `hidden: true` to hide sensitive endpoints 943 | 944 | ### 4. Performance Optimization 945 | 946 | - Use documentation in development, disable in production 947 | - Use caching appropriately 948 | - Avoid deeply nested structures 949 | 950 | ## 📚 FAQ 951 | 952 | ### Q: Documentation not generated? 953 | 954 | A: Check the following: 955 | 1. Is `enable` set to `true` in config file 956 | 2. Is `#[Api]` annotation added to controller 957 | 3. Is route annotation added to method (e.g., `#[GetMapping]`) 958 | 4. Check logs for errors 959 | 960 | ### Q: How to define array types? 961 | 962 | A: Use PHPDoc comments or `ArrayType` annotation: 963 | 964 | ```php 965 | /** 966 | * @var User[] 967 | */ 968 | public array $users; 969 | 970 | // Or 971 | #[ArrayType(User::class)] 972 | public array $users; 973 | ``` 974 | 975 | ### Q: How to hide certain endpoints? 976 | 977 | A: Use `hidden` parameter: 978 | 979 | ```php 980 | #[Api(hidden: true)] // Hide entire controller 981 | 982 | #[ApiOperation(summary: 'Test', hidden: true)] // Hide single endpoint 983 | ``` 984 | 985 | ### Q: How to customize response format? 986 | 987 | A: Use `ApiResponse` annotation or configure global response class: 988 | 989 | ```php 990 | #[ApiResponse(UserResponse::class, 200, 'Success')] 991 | public function getUser(): UserResponse 992 | { 993 | return new UserResponse(); 994 | } 995 | ``` 996 | 997 | ### Q: What validation rules are supported? 998 | 999 | A: All Hyperf Validation rules are supported. See [Hyperf Validation Documentation](https://hyperf.wiki/3.1/#/en/validation). 1000 | 1001 | ### Q: Does `AutoController` annotation work? 1002 | 1003 | A: Yes, but it only collects `POST` methods. It's recommended to use standard route annotations for better documentation generation. 1004 | 1005 | ## 📖 Example Project 1006 | 1007 | > For complete examples, see the [example directory](https://github.com/tw2066/api-docs/tree/master/example) 1008 | 1009 | ## 🔗 Related Links 1010 | 1011 | - [Hyperf Official Documentation](https://hyperf.wiki) 1012 | - [OpenAPI Specification](https://swagger.io/specification/) 1013 | - [Swagger UI](https://swagger.io/tools/swagger-ui/) 1014 | - [Knife4j](https://doc.xiaominfo.com/) 1015 | - [Example Project](https://github.com/tw2066/api-docs/tree/master/example) 1016 | 1017 | ## 📝 Changelog 1018 | 1019 | See [CHANGELOG](CHANGELOG.md) for detailed version updates. 1020 | 1021 | ## 🤝 Contributing 1022 | 1023 | Issues and Pull Requests are welcome! 1024 | 1025 | 1. Fork this repository 1026 | 2. Create a feature branch (`git checkout -b feature/AmazingFeature`) 1027 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 1028 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 1029 | 5. Open a Pull Request 1030 | 1031 | ## 📜 License 1032 | 1033 | [MIT License](LICENSE) 1034 | 1035 | ## ❤️ Acknowledgments 1036 | 1037 | - [Hyperf](https://github.com/hyperf/hyperf) - Excellent coroutine PHP framework 1038 | - [Swagger PHP](https://github.com/zircote/swagger-php) - PHP Swagger generator 1039 | - [Knife4j](https://gitee.com/xiaoym/knife4j) - Excellent API documentation tool 1040 | 1041 | --- 1042 | 1043 | If this project helps you, please give it a ⭐ Star! 1044 | --------------------------------------------------------------------------------