├── .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 | 
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 | [](https://packagist.org/packages/tangwei/apidocs)
4 | [](https://packagist.org/packages/tangwei/apidocs)
5 | [](https://github.com/tw2066/api-docs)
6 | [](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 | [](https://packagist.org/packages/tangwei/apidocs)
4 | [](https://packagist.org/packages/tangwei/apidocs)
5 | [](https://github.com/tw2066/api-docs)
6 | [](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 |
--------------------------------------------------------------------------------