Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2 通过注解来自动配置 webman 路由 #5

Merged
merged 9 commits into from
May 8, 2024
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ composer require webman-tech/swagger

## 特点

- 基于 [zircote/swagger-php](https://github.com/zircote/swagger-php)
- 支持零配置启动
- 基于 [zircote/swagger-php](https://github.com/zircote/swagger-php)(同时支持 Annotation 和 Attribute 模式)
- 支持零配置启动(安装后直接访问 /openapi 即可看到 swagger UI 的界面)
- 支持单应用下多个 swagger 文档(多路由,不同 api 文档)
- 支持动态修改注解下的 swagger 文档(解决注解下无法写动态配置的问题)
- 支持丰富的配置(host 访问限制 / swagger-ui 配置 / openapi 配置)
- 性能优先(服务启动后缓存,开发环境支持自动更新)
- 支持自动注册 webman 路由(已经写了 openapi 文档,再写一遍 webman Route 是不是多此一举?)

## 使用

Expand Down Expand Up @@ -124,3 +125,12 @@ Route::group('/api2', function () {
`app.php` 中的配置是全局的

`(new Swagger())->registerRoute($config)` 中的传参 `$config` 是应用级别的

## webman 路由自动注册

在 config 的 `app.php` 中修改 `register_webman_route` 为 true 即可自动注册 webman 路由


## 参考

[webman 使用 swagger 示例,注解的 crud](https://github.com/webman-tech/webman-samples/tree/swagger-attributions)
102 changes: 41 additions & 61 deletions src/Controller/OpenapiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

namespace WebmanTech\Swagger\Controller;

use Closure;
use Doctrine\Common\Annotations\Annotation;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
use OpenApi\Util;
use Symfony\Component\Finder\Finder;
use Throwable;
use Webman\Http\Response;
use WebmanTech\Swagger\Helper\ArrayHelper;
use WebmanTech\Swagger\DTO\ConfigOpenapiDocDTO;
use WebmanTech\Swagger\DTO\ConfigSwaggerUiDTO;
use WebmanTech\Swagger\Helper\JsExpression;
use WebmanTech\Swagger\RouteAnnotation\Analysers\ReflectionAnalyser;
use WebmanTech\Swagger\RouteAnnotation\Processors\CleanRouteX;

class OpenapiController
{
Expand All @@ -34,74 +36,44 @@ public function __construct()
];
}

public function swaggerUI(string $docRoute, array $config = []): Response
/**
* @param string $docRoute
* @param array|ConfigOpenapiDocDTO $config
* @return Response
* @throws Throwable
*/
public function swaggerUI(string $docRoute, $config = []): Response
{
$config = ArrayHelper::merge(
[
'view' => 'swagger-ui',
'view_path' => '../vendor/webman-tech/swagger/src', // 相对 app_path() 的路径
'data' => [
// @link https://github.com/swagger-api/swagger-ui/blob/master/dist/swagger-initializer.js
'css' => [
'https://unpkg.com/swagger-ui-dist/swagger-ui.css',
//'https://unpkg.com/swagger-ui-dist/index.css',
],
'js' => [
'https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js',
//'https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js',
],
'title' => config('app.name', 'swagger') . ' - openapi',
'ui_config' => [
// @link https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
'dom_id' => '#swagger-ui',
'persistAuthorization' => true,
'deepLinking' => true,
'filter' => '',
/*'presets' => [
new JsExpression('SwaggerUIBundle.presets.apis'),
new JsExpression('SwaggerUIStandalonePreset'),
],
'plugins' => [
new JsExpression('SwaggerUIBundle.plugins.DownloadUrl'),
],
'layout' => 'StandaloneLayout',*/
],
],
],
config('plugin.webman-tech.swagger.app.swagger_ui', []),
$config
);
$config['data']['ui_config']['url'] = new JsExpression("window.location.pathname + '/{$docRoute}'");

return raw_view($config['view'], $config['data'], $config['view_path']);
if (!$config instanceof ConfigSwaggerUiDTO) {
$config = new ConfigSwaggerUiDTO($config);
}

$tempData = $config->data;
$tempData['ui_config']['url'] = new JsExpression("window.location.pathname + '/{$docRoute}'");
$config->data = $tempData;

return raw_view($config->view, $config->data, $config->view_path);
}

private static $docCache = [];

public function openapiDoc(array $config = []): Response
/**
* @param array|ConfigOpenapiDocDTO $config
* @return Response
* @throws Throwable
*/
public function openapiDoc($config = []): Response
{
$config = ArrayHelper::merge(
[
'scan_path' => [], // 扫描的目录
'scan_exclude' => null, // 扫描忽略的
'modify' => null, // 修改 $openapi 对象
'cache_key' => null, // 如何缓存
],
config('plugin.webman-tech.swagger.app.openapi_doc', []),
$config
);

if (is_callable($config['cache_key'])) {
$config['cache_key'] = $config['cache_key']();
if (!$config instanceof ConfigOpenapiDocDTO) {
$config = new ConfigOpenapiDocDTO($config);
}
$cacheKey = $config['cache_key'] ?: __CLASS__;

$cacheKey = $config->getCacheKey();

if (!isset(static::$docCache[$cacheKey])) {
$openapi = $this->scanAndGenerateOpenapi($config['scan_path'], $config['scan_exclude']);
$openapi = $this->scanAndGenerateOpenapi($config->scan_path, $config->scan_exclude);

if ($config['modify'] instanceof Closure) {
$config['modify']($openapi);
}
$config->applyModify($openapi);

$yaml = $openapi->toYaml();

Expand Down Expand Up @@ -134,7 +106,15 @@ private function scanAndGenerateOpenapi($scanPath, $scanExclude = null, int $err
}

try {
return Generator::scan(Util::finder($scanPath, $scanExclude));
/**
* @see Generator::scan
*/
return (new Generator())
->setAliases(Generator::DEFAULT_ALIASES)
->setNamespaces(Generator::DEFAULT_NAMESPACES)
->setAnalyser(new ReflectionAnalyser())
->addProcessor(new CleanRouteX()) // 清理路由注解
->generate(Util::finder($scanPath, $scanExclude));
} catch (Throwable $e) {
if ($errorCount > count($requiredElements)) {
throw $e;
Expand Down
61 changes: 61 additions & 0 deletions src/DTO/BaseDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace WebmanTech\Swagger\DTO;

use WebmanTech\Swagger\Helper\ArrayHelper;

class BaseDTO implements \JsonSerializable
{
protected $_data;

final public function __construct(array $data = [])
{
$this->_data = $data;
$this->initData();
}

protected function initData()
{
}

public function __get($name)
{
return $this->_data[$name] ?? null;
}

public function __set($name, $value)
{
$this->_data[$name] = $value;
}

public function merge(...$data)
{
$toMerge = [];
foreach ($data as $items) {
if (!is_array($items) || $items === []) {
continue;
}
$toMerge[] = $items;
}
if (!$toMerge) {
return;
}

$this->_data = ArrayHelper::merge(
$this->toArray(),
...$toMerge
);
$this->initData();
}

public function toArray(): array
{
return $this->_data;
}

#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
}
}
28 changes: 28 additions & 0 deletions src/DTO/ConfigHostForbiddenDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace WebmanTech\Swagger\DTO;

use WebmanTech\Swagger\Helper\ConfigHelper;

/**
* @property bool $enable
* @property bool|null $ip_white_list_intranet 是否允许所有内网访问,为 null 时不检查
* @property array|null $ip_white_list 允许访问的指定 ip,为 null 时不检查
* @property array|null $host_white_list 允许访问的指定 host,为 null 时不检查
*/
class ConfigHostForbiddenDTO extends BaseDTO
{
protected function initData()
{
$this->_data = array_merge(
[
'enable' => true,
'ip_white_list_intranet' => true,
'ip_white_list' => [],
'host_white_list' => [],
],
ConfigHelper::get('app.host_forbidden', []),
$this->_data
);
}
}
49 changes: 49 additions & 0 deletions src/DTO/ConfigOpenapiDocDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace WebmanTech\Swagger\DTO;

use OpenApi\Annotations\OpenApi;
use WebmanTech\Swagger\Helper\ArrayHelper;
use WebmanTech\Swagger\Helper\ConfigHelper;

/**
* @property string|array $scan_path 扫描的目录
* @property null|array $scan_exclude 扫描忽略的
* @property null|callable $modify 修改 $openapi 对象
* @property null|string|callable $cache_key 缓存用的 key,当注册不同实例时,需要指定不同的 key,或者做热更新用
*/
class ConfigOpenapiDocDTO extends BaseDTO
{
protected function initData()
{
$this->_data = ArrayHelper::merge(
[
'scan_path' => [],
'scan_exclude' => null,
'modify' => null,
'cache_key' => null,
],
ConfigHelper::get('app.openapi_doc', []),
$this->_data
);
}

public function getCacheKey(): string
{
$cacheKey = null;
if (is_string($this->cache_key)) {
$cacheKey = $this->cache_key;
} elseif (is_callable($this->cache_key)) {
$cacheKey = call_user_func($this->cache_key);
}

return $cacheKey ?? __CLASS__;
}

public function applyModify(OpenApi $openapi)
{
if (is_callable($this->modify)) {
call_user_func($this->modify, $openapi);
}
}
}
36 changes: 36 additions & 0 deletions src/DTO/ConfigRegisterRouteDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace WebmanTech\Swagger\DTO;

/**
* @property bool $enable 是否启用
* @property string $route_prefix openapi 文档的路由前缀
* @property ConfigHostForbiddenDTO $host_forbidden
* @property ConfigSwaggerUiDTO $swagger_ui
* @property ConfigOpenapiDocDTO $openapi_doc
* @property bool $register_webman_route 是否注册 webman 的路由
*/
class ConfigRegisterRouteDTO extends BaseDTO
{
public function initData()
{
$this->_data = array_merge([
'enable' => true,
'route_prefix' => '/openapi',
'host_forbidden' => new ConfigHostForbiddenDTO(),
'swagger_ui' => new ConfigSwaggerUiDTO(),
'openapi_doc' => new ConfigOpenapiDocDTO(),
'register_webman_route' => false,
], $this->_data);

if (is_array($this->host_forbidden)) {
$this->host_forbidden = new ConfigHostForbiddenDTO($this->host_forbidden);
}
if (is_array($this->swagger_ui)) {
$this->swagger_ui = new ConfigSwaggerUiDTO($this->swagger_ui);
}
if (is_array($this->openapi_doc)) {
$this->openapi_doc = new ConfigOpenapiDocDTO($this->openapi_doc);
}
}
}
Loading