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