diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e846cb4e..d610f15f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,57 +1,111 @@ -name: PHPUnit +name: PHPUnit for MineAdmin -on: [ push, pull_request ] - -env: - SWOOLE_VERSION: '5.1.1' - SWOW_VERSION: 'develop' +on: + push: + pull_request: + schedule: + - cron: '0 2 * * *' jobs: - ci: - name: Test PHP ${{ matrix.php-version }} on ${{ matrix.engine }} + cs-fix: + name: PHP CS Fix on PHP${{ matrix.php }} ${{ matrix.swoole }} + runs-on: ubuntu-latest + strategy: + matrix: + os: [ ubuntu-latest ] + php: [ '8.1','8.2','8.3' ] + swoole: [ 'swoole'] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + tools: php-cs-fixer + extensions: redis, pdo, pdo_mysql, bcmath, ${{ matrix.swoole }} + - name: Setup Packages + run: composer update -oW + - name: Run CS Fix + run: | + vendor/bin/php-cs-fixer fix src --dry-run --diff + vendor/bin/php-cs-fixer fix src --dry-run --diff + tests: + needs: cs-fix + name: Test on PHP${{ matrix.php-version }} Swoole-${{ matrix.sw-version }} runs-on: "${{ matrix.os }}" strategy: matrix: os: [ ubuntu-latest ] - php-version: ['8.1','8.2','8.3' ] - engine: [ 'swoole' ] - max-parallel: 5 - services: - redis: - image: redis - ports: - - "6379:6379" + php-version: [ '8.3', '8.2', '8.1' ] + sw-version: [ 'v5.0.3', 'v5.1.2', 'master' ] + exclude: + - php-version: '8.3' + sw-version: 'v5.0.3' + max-parallel: 20 + fail-fast: false + env: + SW_VERSION: ${{ matrix.sw-version }} + MYSQL_VERSION: '8.0' + PGSQL_VERSION: '14' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 + - name: Upgrade + run: | + sudo apt-get clean + sudo apt-get update + sudo apt-get upgrade -f - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} tools: phpize + extensions: redis, pdo, pdo_mysql, bcmath ini-values: opcache.enable_cli=0 - coverage: none - name: Setup Swoole - if: ${{ matrix.engine == 'swoole' }} run: | - cd /tmp - sudo apt-get update - sudo apt-get install libcurl4-openssl-dev - wget https://github.com/swoole/swoole-src/archive/v${SWOOLE_VERSION}.tar.gz -O swoole.tar.gz + sudo apt-get install libcurl4-openssl-dev libc-ares-dev libpq-dev + wget https://github.com/swoole/swoole-src/archive/${SW_VERSION}.tar.gz -O swoole.tar.gz mkdir -p swoole tar -xf swoole.tar.gz -C swoole --strip-components=1 rm swoole.tar.gz cd swoole phpize - ./configure --enable-openssl --enable-http2 --enable-swoole-curl --enable-swoole-json + ./configure --enable-openssl --enable-swoole-curl --enable-cares --enable-swoole-pgsql --enable-brotli make -j$(nproc) sudo make install sudo sh -c "echo extension=swoole > /etc/php/${{ matrix.php-version }}/cli/conf.d/swoole.ini" + sudo sh -c "echo swoole.use_shortname='Off' >> /etc/php/${{ matrix.php-version }}/cli/conf.d/swoole.ini" php --ri swoole - name: Setup Packages - run: composer update -o --no-scripts - - name: Run Test Cases + run: ./.travis/requirement.install.sh + - name: Run PHPStan + run: ./.travis/run.check.sh + - name: Setup Services + run: ./.travis/setup.services.sh + - name: Setup Mysql + run: export TRAVIS_BUILD_DIR=$(pwd) && bash ./.travis/setup.mysql.sh + - name: Setup PostgreSQL + run: export TRAVIS_BUILD_DIR=$(pwd) && bash ./.travis/setup.pgsql.sh + - name: Run Scripts Before Test + run: cp .travis/.env.example .env + - name: Print PHP Environments run: | - vendor/bin/php-cs-fixer fix --dry-run # cs-fixer 格式化代码 - composer analyse # phpstan 静态代码分析 - composer test # phpunit \ No newline at end of file + php -i + php -m + - name: Run Test Cases + env: + DB_DRIVER: mysql + DB_HOST: 127.0.0.1 + DB_DATABASE: mineadmin + run: ./.travis/run.test.sh +# - name: Run PgSql Test Cases +# env: +# DB_DRIVER: pgsql +# DB_HOST: 127.0.0.1 +# DB_PORT: 5432 +# DB_USERNAME: postgres +# DB_PASSWORD: postgres +# DB_CHARSET: utf8 +# DB_DATABASE: mineadmin +# run: ./.travis/run.test.sh \ No newline at end of file diff --git a/.travis/.env.example b/.travis/.env.example new file mode 100755 index 00000000..62497b5a --- /dev/null +++ b/.travis/.env.example @@ -0,0 +1,14 @@ +APP_NAME=mineadmin +APP_ENV=dev +APP_DEBUG=true +SUPER_ADMIN = 1000 +ADMIN_ROLE = 1000 +CONSOLE_SQL = false +AMQP_HOST = 127.0.0.1 +AMQP_PORT = 5672 +AMQP_USER = mineadmin +AMQP_PASSWORD = mineadmin +AMQP_VHOST = / +AMQP_ENABLE = false +JWT_SECRET="mGlQxdNYoXIzVI0OkqQMaW07TpP94NUcjklspzEY6jXVeSparSQQ70kjlodwov2oINKluPuxgS7uetxaIJof4A==" +JWT_API_SECRET="HLbcdGtYle+H0b18fLSaSdXrj/sSYoFfDMW0zqF/wf0ZgS0HxlqVzoQL2ocNLTsgP+v9EbyOoGghv94A2cGhkg==" diff --git a/.travis/ci.ini b/.travis/ci.ini new file mode 100755 index 00000000..9c87bafa --- /dev/null +++ b/.travis/ci.ini @@ -0,0 +1,8 @@ +[opcache] +opcache.enable_cli=1 + +[redis] +extension = "redis.so" + +[swoole] +extension = "swoole.so" \ No newline at end of file diff --git a/.travis/requirement.install.sh b/.travis/requirement.install.sh new file mode 100755 index 00000000..8fc41319 --- /dev/null +++ b/.travis/requirement.install.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env sh + +set -e + +set -x + +composer install + +php ./.travis/run.replace.php \ No newline at end of file diff --git a/.travis/run.check.sh b/.travis/run.check.sh new file mode 100755 index 00000000..ab17f206 --- /dev/null +++ b/.travis/run.check.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e + +composer analyse src \ No newline at end of file diff --git a/.travis/run.code-coverage.sh b/.travis/run.code-coverage.sh new file mode 100755 index 00000000..7b6127ae --- /dev/null +++ b/.travis/run.code-coverage.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -e +php bin/hyperf.php migrate --path=app/Setting/Database/Migrations + +php bin/hyperf.php migrate --path=app/System/Database/Migrations + +php bin/hyperf.php db:seed --path=app/Setting/Database/Seeders + +php bin/hyperf.php db:seed --path=app/System/Database/Seeders + +php bin/hyperf.php mine:update + +composer coverage \ No newline at end of file diff --git a/.travis/run.replace.php b/.travis/run.replace.php new file mode 100755 index 00000000..b9f32317 --- /dev/null +++ b/.travis/run.replace.php @@ -0,0 +1,15 @@ + ~/.pgpass +chmod 600 ~/.pgpass + +docker exec postgres psql -d postgres -U postgres -c "create database mineadmin" + +echo -e "Done\n" + +wait \ No newline at end of file diff --git a/.travis/setup.services.sh b/.travis/setup.services.sh new file mode 100755 index 00000000..0171ef84 --- /dev/null +++ b/.travis/setup.services.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +docker run --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=true -d mysql:${MYSQL_VERSION} --bind-address=0.0.0.0 --default-authentication-plugin=mysql_native_password & +docker run --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres:${PGSQL_VERSION} & +docker run --name redis -p 6379:6379 -d redis & +docker run -d --restart=always --name rabbitmq -e RABBITMQ_DEFAULT_USER=mineadmin -e RABBITMQ_DEFAULT_PASS=123456 -p 4369:4369 -p 5672:5672 -p 15672:15672 -p 25672:25672 rabbitmq:management-alpine & +wait +sleep 10 +docker ps -a \ No newline at end of file diff --git a/CHANGELOG-2.0.md b/CHANGELOG-2.0.md new file mode 100644 index 00000000..8c2fd6cc --- /dev/null +++ b/CHANGELOG-2.0.md @@ -0,0 +1,7 @@ +# v2.0 - TBD + +# v2.0.0-RC 25 March 2024 + +## Added + +- [#53](https://github.com/mineadmin/components/pull/53) Splitting components http-server \ No newline at end of file diff --git a/CHANGELOG-2.0.zh_CN.md b/CHANGELOG-2.0.zh_CN.md new file mode 100644 index 00000000..38ff172e --- /dev/null +++ b/CHANGELOG-2.0.zh_CN.md @@ -0,0 +1,7 @@ +# v2.0 - TBD + +# v2.0.0-RC 2024年3月25日 + +## Added + +- [#53](https://github.com/mineadmin/components/pull/53) 拆分组件 http-server \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..04a43bcd --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# 项目介绍 + +

+ +

+

+ 官网 | + 文档 | + 演示 | + Hyperf官方文档 +

+ +

+ + + + +

\ No newline at end of file diff --git a/composer.json b/composer.json index 0f24766f..1fc3cca4 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "Mine\\Helper\\": "src/mine-helpers/src", "Mine\\Generator\\": "src/mine-generator/src", "Xmo\\AppStore\\": "src/app-store/src", - "Mine\\NextCoreX\\": "src/next-core-x/src" + "Mine\\NextCoreX\\": "src/next-core-x/src", + "Mine\\HttpServer\\": "src/HttpServer/src" }, "files": [ "src/mine-helpers/src/functions.php" @@ -30,7 +31,8 @@ "Mine\\Tests\\": "tests", "Xmo\\AppStore\\Tests\\": "src/app-store/tests", "Xmo\\MineCore\\Tests\\": "src/mine-core/tests", - "Mine\\NextCoreX\\Tests\\": "src/next-core-x/tests" + "Mine\\NextCoreX\\Tests\\": "src/next-core-x/tests", + "Mine\\HttpServer\\Tests\\": "src/HttpServer/tests" } }, "authors": [ @@ -54,7 +56,8 @@ "xmo/mine-helpers": "*", "xmo/mine-genertor": "*", "xmo/app-store": "*", - "mine/next-core-x": "*" + "mine/next-core-x": "*", + "mineadmin/http-server": "*" }, "require": { "php": ">=8.1", diff --git a/phpstan.neon b/phpstan.neon index e0bd1d0a..27a0467d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -17,6 +17,7 @@ parameters: - src/mine-core/tests/* - src/tests/ - src/app-store/test/ + - src/httpServer/tests/ ignoreErrors: - '#Unsafe usage of new static\(\)#' - '#Call to static method find\(\) on an unknown class#' diff --git a/phpunit.xml b/phpunit.xml index dc91c4e7..4d219de5 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,18 +3,11 @@ + ./src/HttpServer/tests ./src/app-store/tests ./src/mine-core/tests ./src/next-core-x/tests ./tests - - - ./src/app-store/tests - ./src/mine-core/tests - ./src/next-core-x/tests - ./tests - - diff --git a/src/HttpServer/.github/workflows/close-pull-request.yml b/src/HttpServer/.github/workflows/close-pull-request.yml new file mode 100644 index 00000000..c8182b84 --- /dev/null +++ b/src/HttpServer/.github/workflows/close-pull-request.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [ opened ] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Hi, this is a READ-ONLY repository, please submit your PR on the https://github.com/mineadmin/components repository.

This Pull Request will close automatically.

Thanks! " \ No newline at end of file diff --git a/src/HttpServer/.github/workflows/release.yml b/src/HttpServer/.github/workflows/release.yml new file mode 100644 index 00000000..2fc8404b --- /dev/null +++ b/src/HttpServer/.github/workflows/release.yml @@ -0,0 +1,24 @@ +on: + push: + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + +name: Release + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false \ No newline at end of file diff --git a/src/HttpServer/IdeHelpers/RequestIde.php b/src/HttpServer/IdeHelpers/RequestIde.php new file mode 100644 index 00000000..e824579d --- /dev/null +++ b/src/HttpServer/IdeHelpers/RequestIde.php @@ -0,0 +1,31 @@ +=8.1", + "hyperf/http-server": "^3.1", + "hyperf/validation": "^3.1", + "hyperf/http-message": "^3.1", + "ramsey/uuid": "^4.7", + "hyperf/translation": "^3.1" + }, + "autoload": { + "psr-4": { + "Mine\\HttpServer\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Mine\\HttpServer\\Tests\\": "tests/" + } + }, + "extra": { + "hyperf": { + "config": "Mine\\HttpServer\\ConfigProvider" + } + } +} \ No newline at end of file diff --git a/src/HttpServer/publish/languages/en/result.php b/src/HttpServer/publish/languages/en/result.php new file mode 100644 index 00000000..eeea95a3 --- /dev/null +++ b/src/HttpServer/publish/languages/en/result.php @@ -0,0 +1,44 @@ + 'Request success', + 'failed' => 'Request failed', + 'token' => [ + 'expired' => 'TOKEN expired or does not exist', + ], + 'validate' => [ + 'failed' => 'Data validation failed', + ], + 'no_permission' => 'No permission', + 'no_data' => 'No data', + 'normal_status' => 'Normal status abnormal code', + 'no_user' => 'User does not exist', + 'password_error' => 'Wrong password', + 'user_ban' => 'User is banned', + 'method_not_allow' => 'The address uses an unacceptable access method', + 'not_found' => 'Resource does not exist', + 'interface_exception' => 'Interface exception', + 'resource_stop' => 'Resource is disabled', + 'app_ban' => 'APP is disabled', + 'api_auth_exception' => 'Interface authentication exception', + 'api_auth_fail' => 'Interface authentication failed', + 'api_unbind_app' => 'The interface is not bound to the application', + 'api_app_id_missing' => 'Missing app_id', + 'api_app_secret_missing' => 'Missing APP SECRET', + 'api_access_token_missing' => 'Missing access token in complex mode', + 'api_params_error' => 'Parameter error in complex mode ACCESS TOKEN', + 'api_sign_missing' => 'Missing signature', + 'api_sign_error' => 'Signature error', + 'api_identity_missing' => 'Missing identity in simplified mode', + 'api_identity_error' => 'Identity error in simplified mode', + 'api_verify_pass' => 'API verification passed', +]; diff --git a/src/HttpServer/publish/languages/zh_CN/result.php b/src/HttpServer/publish/languages/zh_CN/result.php new file mode 100644 index 00000000..7df488c0 --- /dev/null +++ b/src/HttpServer/publish/languages/zh_CN/result.php @@ -0,0 +1,44 @@ + '请求成功', + 'failed' => '请求失败', + 'token' => [ + 'expired' => 'TOKEN过期、不存在', + ], + 'validate' => [ + 'failed' => '数据验证失败', + ], + 'no_permission' => '没有权限', + 'no_data' => '没有数据', + 'normal_status' => '正常状态异常代码', + 'no_user' => '用户不存在', + 'password_error' => '密码错误', + 'user_ban' => '用户被禁', + 'method_not_allow' => '地址使用了不允许的访问方法', + 'not_found' => '资源不存在', + 'interface_exception' => '接口异常', + 'resource_stop' => '资源被停用', + 'app_ban' => 'APP被停用', + 'api_auth_exception' => '接口鉴权异常', + 'api_auth_fail' => '接口鉴权失败', + 'api_unbind_app' => '接口未被该应用绑定', + 'api_app_id_missing' => '缺少 app_id', + 'api_app_secret_missing' => '缺少APP SECRET', + 'api_access_token_missing' => '缺少复杂模式 ACCESS TOKEN', + 'api_params_error' => '复杂模式 ACCESS TOKEN 参数错误', + 'api_sign_missing' => '缺少签名', + 'api_sign_error' => '签名错误', + 'api_identity_missing' => '缺少简易模式 identity', + 'api_identity_error' => '简易模式 identity 错误', + 'api_verify_pass' => 'API验证通过', +]; diff --git a/src/HttpServer/src/Config.php b/src/HttpServer/src/Config.php new file mode 100644 index 00000000..679ca585 --- /dev/null +++ b/src/HttpServer/src/Config.php @@ -0,0 +1,29 @@ +config->get(self::PREFIX . '.' . $key, $default); + } +} diff --git a/src/HttpServer/src/ConfigProvider.php b/src/HttpServer/src/ConfigProvider.php new file mode 100644 index 00000000..d07dbbbc --- /dev/null +++ b/src/HttpServer/src/ConfigProvider.php @@ -0,0 +1,58 @@ + [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + // 默认 Command 的定义,合并到 Hyperf\Contract\ConfigInterface 内,换个方式理解也就是与 config/autoload/commands.php 对应 + 'commands' => [], + // 与 commands 类似 + 'listeners' => [ + BootApplicationListener::class, + ], + // 合并到 config/autoload/dependencies.php 文件 + 'dependencies' => [ + RequestIdGeneratorInterface::class => RequestIdGenerator::class, + ], + 'publish' => [ + [ + 'id' => 'MineAdmin-HttpServer-Trans', + 'description' => 'MineAdmin Response Code Translation File', + 'source' => __DIR__ . '/../publish/languages/en/result.php', + 'destination' => BASE_PATH . '/storage/languages/en/result.php', + ], + [ + 'id' => 'MineAdmin-HttpServer-Trans zh_CN', + 'description' => 'MineAdmin Response Code Translation File', + 'source' => __DIR__ . '/../publish/languages/zh_CN/result.php', + 'destination' => BASE_PATH . '/storage/languages/zh_CN/result.php', + ], + ], + ]; + } +} diff --git a/src/HttpServer/src/Constant/HttpResultCode.php b/src/HttpServer/src/Constant/HttpResultCode.php new file mode 100644 index 00000000..6914d53f --- /dev/null +++ b/src/HttpServer/src/Constant/HttpResultCode.php @@ -0,0 +1,160 @@ +getTrans()) ? $message : $code->getTrans(); + $code = $code->value; + } + parent::__construct($message, $code, $previous); + } +} diff --git a/src/HttpServer/src/Exception/Handler/HttpExceptionHandler.php b/src/HttpServer/src/Exception/Handler/HttpExceptionHandler.php new file mode 100644 index 00000000..6b76c924 --- /dev/null +++ b/src/HttpServer/src/Exception/Handler/HttpExceptionHandler.php @@ -0,0 +1,45 @@ +getMessage(), + code: $throwable->getCode() + ); + return $response + ->setBody( + new SwooleStream((string) $result) + ); + } + + public function isValid(\Throwable $throwable): bool + { + return $throwable instanceof HttpException; + } +} diff --git a/src/HttpServer/src/Exception/HttpException.php b/src/HttpServer/src/Exception/HttpException.php new file mode 100644 index 00000000..049140ef --- /dev/null +++ b/src/HttpServer/src/Exception/HttpException.php @@ -0,0 +1,15 @@ +merge(__FUNCTION__); + } + + public function attributes(): array + { + return $this->merge(__FUNCTION__); + } + + public function rules(): array + { + return $this->merge(__FUNCTION__); + } + + private function merge(string $function): array + { + $commonFunc = 'common' . ucfirst($function); + $actionFunc = $this->getAction() . ucfirst($function); + return array_merge( + $this->{$commonFunc}(), + $this->{$actionFunc}() + ); + } +} diff --git a/src/HttpServer/src/Listener/BootApplicationListener.php b/src/HttpServer/src/Listener/BootApplicationListener.php new file mode 100644 index 00000000..29e25e6f --- /dev/null +++ b/src/HttpServer/src/Listener/BootApplicationListener.php @@ -0,0 +1,84 @@ +registerRequestMacro(); + $this->registerResponseMacro(); + } + + private function registerResponseMacro(): void + { + ApplicationContext::getContainer()->set(Response::class, MineResponse::class); + } + + private function registerRequestMacro(): void + { + Request::macro('ip', function () { + /** + * @var Request $this + */ + $ip = $this->getServerParams()['remote_addr'] ?? '0.0.0.0'; + $headers = $this->getHeaders(); + if (isset($headers['x-real-ip'])) { + $ip = $headers['x-real-ip'][0]; + } elseif (isset($headers['x-forwarded-for'])) { + $ip = $headers['x-forwarded-for'][0]; + } elseif (isset($headers['http_x_forwarded_for'])) { + $ip = $headers['http_x_forwarded_for'][0]; + } elseif (isset($headers['remote_host'])) { + $ip = $headers['remote_host'][0]; + } + return $ip; + }); + + Request::macro('getAction', function () { + /** + * @var Dispatched $dispatch + * @var Request $this + */ + $dispatch = $this->getAttribute(Dispatched::class); + $callback = $dispatch?->handler?->callback; + if (is_array($callback) && count($callback) === 2) { + return $callback[1]; + } + if (is_string($callback)) { + if (str_contains($callback, '@')) { + return explode('@', $callback)[1] ?? null; + } + if (str_contains($callback, '::')) { + return explode('::', $callback)[1] ?? null; + } + } + return null; + }); + } +} diff --git a/src/HttpServer/src/Log/Processor/RequestIdProcessor.php b/src/HttpServer/src/Log/Processor/RequestIdProcessor.php new file mode 100644 index 00000000..b85e18b7 --- /dev/null +++ b/src/HttpServer/src/Log/Processor/RequestIdProcessor.php @@ -0,0 +1,26 @@ +toString(); + } + return (string) $this->container->get(IdGeneratorInterface::class)->generate(); + }); + } +} diff --git a/src/HttpServer/src/Middleware/CorsMiddleware.php b/src/HttpServer/src/Middleware/CorsMiddleware.php new file mode 100644 index 00000000..b4fe28fe --- /dev/null +++ b/src/HttpServer/src/Middleware/CorsMiddleware.php @@ -0,0 +1,36 @@ +withHeader('Access-Control-Allow-Origin', '*') + ->withHeader('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS') + ->withHeader('Access-Control-Allow-Credentials', 'true') + ->withHeader('Access-Control-Allow-Headers', 'accept-language,authorization,lang,uid,token,Keep-Alive,User-Agent,Cache-Control,Content-Type'); + Context::set(ResponseInterface::class, $response); + if ($request->getMethod() === 'OPTIONS') { + return $response; + } + return $handler->handle($request); + } +} diff --git a/src/HttpServer/src/Middleware/I18nMiddleware.php b/src/HttpServer/src/Middleware/I18nMiddleware.php new file mode 100644 index 00000000..f42e9f56 --- /dev/null +++ b/src/HttpServer/src/Middleware/I18nMiddleware.php @@ -0,0 +1,61 @@ +hasHeader(self::HTTP_HEADER_KEY)) { + return $handler->handle($request); + } + $acceptLanguage = $request->getHeaderLine(self::HTTP_HEADER_KEY); + $locales = $this->getLocales(); + if (! in_array($acceptLanguage, $locales, true)) { + return $handler->handle($request); + } + $this->translator->setLocale($acceptLanguage); + return $handler->handle($request); + } + + private function getLocales(): array + { + $appLocales = $this->config->get('translatable.locales', []); + $locales = []; + foreach ($appLocales as $i => $v) { + if (is_int($i)) { + $locales[] = $v; + } + if (is_string($i) && is_array($v)) { + foreach ($v as $sub) { + $locales[] = $i . '_' . $sub; + } + } + } + return $locales; + } +} diff --git a/src/HttpServer/src/Middleware/JsonMiddleware.php b/src/HttpServer/src/Middleware/JsonMiddleware.php new file mode 100644 index 00000000..48eeed39 --- /dev/null +++ b/src/HttpServer/src/Middleware/JsonMiddleware.php @@ -0,0 +1,26 @@ +handle($request)->withHeader('content-type', 'application/json; charset=utf-8'); + } +} diff --git a/src/HttpServer/src/Middleware/RequestIdMiddleware.php b/src/HttpServer/src/Middleware/RequestIdMiddleware.php new file mode 100644 index 00000000..2c2706bd --- /dev/null +++ b/src/HttpServer/src/Middleware/RequestIdMiddleware.php @@ -0,0 +1,28 @@ +handle($request); + } +} diff --git a/src/HttpServer/src/RequestIdHolder.php b/src/HttpServer/src/RequestIdHolder.php new file mode 100644 index 00000000..78989f7d --- /dev/null +++ b/src/HttpServer/src/RequestIdHolder.php @@ -0,0 +1,25 @@ +get(RequestIdGeneratorInterface::class)->generate(); + } +} diff --git a/src/HttpServer/src/Result.php b/src/HttpServer/src/Result.php new file mode 100644 index 00000000..ac7108d8 --- /dev/null +++ b/src/HttpServer/src/Result.php @@ -0,0 +1,128 @@ +handleCode(); + } + + public function __toString(): string + { + return Json::encode($this->toArray()); + } + + public function isSuccess(): bool + { + return $this->success; + } + + public function setSuccess(bool $success): void + { + $this->success = $success; + } + + public function getMessage(): ?string + { + return $this->message; + } + + public function setMessage(?string $message): void + { + $this->message = $message; + } + + public function getCode(): null|HttpResultCode|int + { + return $this->code; + } + + public function setCode(null|HttpResultCode|int $code): void + { + $this->code = $code; + $this->handleCode(); + } + + public function getData(): ?array + { + return $this->data; + } + + public function setData(?array $data): void + { + $this->data = $data; + } + + public function toArray(): array + { + $result = [ + 'success' => $this->isSuccess(), + 'requestId' => RequestIdHolder::getId(), + ]; + if ($this->getMessage()) { + $result['message'] = $this->getMessage(); + } + if ($this->getData()) { + $result['data'] = $this->getData(); + } + if ($this->getCode()) { + $result['code'] = $this->getCode(); + } + return $result; + } + + public static function success( + ?string $message = null, + array|object $data = [], + HttpResultCode|int $code = 200 + ): self { + return new self( + success: true, + message: $message, + code: $code, + data: $data + ); + } + + public static function error( + ?string $message = null, + array|object $data = [], + HttpResultCode|int $code = 200 + ): self { + return new self( + success: false, + message: $message, + code: $code, + data: $data + ); + } + + private function handleCode(): void + { + if ($this->getCode() instanceof HttpResultCode) { + $trans = $this->getCode()->getTrans(); + $this->setMessage(empty($trans) ? $this->getMessage() : $trans); + $this->setCode($this->getCode()->value); + } + } +} diff --git a/src/HttpServer/src/Server.php b/src/HttpServer/src/Server.php new file mode 100644 index 00000000..6f1f95ec --- /dev/null +++ b/src/HttpServer/src/Server.php @@ -0,0 +1,17 @@ +assertIsArray((new ConfigProvider())()); + } +} diff --git a/src/HttpServer/tests/Cases/Exception/BusinessExceptionTest.php b/src/HttpServer/tests/Cases/Exception/BusinessExceptionTest.php new file mode 100644 index 00000000..59d830c0 --- /dev/null +++ b/src/HttpServer/tests/Cases/Exception/BusinessExceptionTest.php @@ -0,0 +1,70 @@ + [ + LogLevel::DEBUG, + ], + ]); + ApplicationContext::getContainer() + ->set(ConfigInterface::class, $config); + ApplicationContext::getContainer()->define( + RequestIdGeneratorInterface::class, + RequestIdGenerator::class + ); + ApplicationContext::getContainer() + ->define( + RequestIdGeneratorInterface::class, + RequestIdGenerator::class + ); + } + + public function testConstruct(): void + { + $translator = \Mockery::mock(TranslatorInterface::class); + $translator->allows('trans') + ->withArgs(function ($key) { + return true; + }) + ->andReturn('xxx'); + ApplicationContext::getContainer()->set(TranslatorInterface::class, $translator); + $businessException = new BusinessException('xxx', 100); + $this->assertSame($businessException->getMessage(), 'xxx'); + $this->assertSame($businessException->getCode(), 100); + + $businessException = new BusinessException(code: HttpResultCode::SUCCESS); + $this->assertSame($businessException->getCode(), HttpResultCode::SUCCESS->value); + $this->assertSame($businessException->getMessage(), 'xxx'); + } +} diff --git a/src/HttpServer/tests/Cases/Exception/Handler/HttpExceptionHandlerTest.php b/src/HttpServer/tests/Cases/Exception/Handler/HttpExceptionHandlerTest.php new file mode 100644 index 00000000..8a259f5c --- /dev/null +++ b/src/HttpServer/tests/Cases/Exception/Handler/HttpExceptionHandlerTest.php @@ -0,0 +1,72 @@ +define( + RequestIdGeneratorInterface::class, + RequestIdGenerator::class + ); + } + + public function testIsValid(): void + { + $reflection = new \ReflectionClass(HttpExceptionHandler::class); + $method = $reflection->getMethod('isValid'); + $instance = \Mockery::mock(HttpExceptionHandler::class); + $this->assertTrue($method->invoke($instance, \Mockery::mock(HttpException::class))); + $this->assertFalse($method->invoke($instance, new \Exception())); + $this->assertTrue($method->invoke($instance, new class() extends HttpException {})); + } + + public function testHandle(): void + { + $requestId = RequestIdHolder::getId(); + $reflection = new \ReflectionClass(HttpExceptionHandler::class); + $method = $reflection->getMethod('handle'); + $instance = \Mockery::mock(HttpExceptionHandler::class); + $response = \Mockery::mock(ResponsePlusInterface::class); + $response->allows('setBody')->withArgs(function (SwooleStream $stream) use ($requestId) { + $this->assertInstanceOf(SwooleStream::class, $stream); + $this->assertSame($stream->getContents(), '{"success":false,"requestId":"' . $requestId . '","message":"xxx","code":100}'); + return true; + })->andReturn($response); + $method->invoke( + $instance, + new class() extends HttpException { + protected $message = 'xxx'; + + protected $code = 100; + }, + $response + ); + } +} diff --git a/src/HttpServer/tests/Cases/HttpRequestTest.php b/src/HttpServer/tests/Cases/HttpRequestTest.php new file mode 100644 index 00000000..2c818ba8 --- /dev/null +++ b/src/HttpServer/tests/Cases/HttpRequestTest.php @@ -0,0 +1,114 @@ +process(new BootApplication()); + parent::setUp(); // TODO: Change the autogenerated stub + } + + public function testIp() + { + $request = new Request(); + $interface = \Mockery::mock(ServerRequestPlusInterface::class); + $interface->allows('getHeaders')->andReturns( + [ + 'x-real-ip' => ['127.0.0.1'], + 'x-forwarded-for' => ['127.0.0.2'], + ], + [ + 'x-forwarded-for' => ['127.0.0.2'], + 'http_x_forwarded_for' => ['127.0.0.3'], + ], + [ + 'http_x_forwarded_for' => ['127.0.0.3'], + 'remote_host' => ['127.0.0.4'], + ], + [ + 'remote_host' => ['127.0.0.4'], + ], + ); + $interface->allows('getServerParams')->andReturns([]); + RequestContext::set($interface); + self::assertSame($request->ip(), '127.0.0.1'); + self::assertSame($request->ip(), '127.0.0.2'); + self::assertSame($request->ip(), '127.0.0.3'); + self::assertSame($request->ip(), '127.0.0.4'); + } + + public function testSchema() + { + $request = new Request(); + $interface = \Mockery::mock(ServerRequestPlusInterface::class); + $interface->allows('getUri')->andReturns( + new Uri('https://baidu.com/?q=xxx'), + new Uri('http://baidu.com/?q=xxx'), + ); + RequestContext::set($interface); + $this->assertSame($request->getUri()->getScheme(), 'https'); + $this->assertSame($request->getUri()->getScheme(), 'http'); + } + + public function testGetAction() + { + $request = new Request(); + $interface = \Mockery::mock(ServerRequestPlusInterface::class); + $dispatch = new Dispatched([ + Dispatcher::FOUND, + new Handler([ + 'error', 'index', + ], 'test'), + [], + ]); + $dispatch1 = new Dispatched([ + Dispatcher::FOUND, + new Handler('index::index', 'test'), + [], + ]); + + $dispatch2 = new Dispatched([ + Dispatcher::FOUND, + new Handler('index@index', 'test'), + [], + ]); + $interface + ->allows('getAttribute') + ->andReturn(new Dispatched([ + Dispatcher::NOT_FOUND, + ]), $dispatch, $dispatch1, $dispatch2); + RequestContext::set($interface); + $this->assertNull($request->getAction()); + $this->assertSame($request->getAction(), 'index'); + $this->assertSame($request->getAction(), 'index'); + $this->assertSame($request->getAction(), 'index'); + } +} diff --git a/src/HttpServer/tests/Cases/Log/Processor/RequestIdProcessorTest.php b/src/HttpServer/tests/Cases/Log/Processor/RequestIdProcessorTest.php new file mode 100644 index 00000000..58dd0737 --- /dev/null +++ b/src/HttpServer/tests/Cases/Log/Processor/RequestIdProcessorTest.php @@ -0,0 +1,52 @@ +define( + RequestIdGeneratorInterface::class, + RequestIdGenerator::class + ); + } + + public function testInvoke(): void + { + $requestIdProcessor = new RequestIdProcessor(); + $logRecord = new LogRecord( + \Mockery::mock(\DateTimeImmutable::class), + 'xxx', + Level::Alert, + 'xxx' + ); + $requestIdProcessor($logRecord); + $this->assertArrayHasKey('request_id', $logRecord->extra); + $this->assertSame($logRecord->extra['request_id'], RequestIdHolder::getId()); + } +} diff --git a/src/HttpServer/tests/Cases/Log/RequestIdGeneratorTest.php b/src/HttpServer/tests/Cases/Log/RequestIdGeneratorTest.php new file mode 100644 index 00000000..b291f482 --- /dev/null +++ b/src/HttpServer/tests/Cases/Log/RequestIdGeneratorTest.php @@ -0,0 +1,40 @@ +get(RequestIdGenerator::class); + $id = $generator->generate(); + self::assertIsString($id); + self::assertEquals($id, $generator->generate()); + Coroutine::create(function () use ($id) { + $generator = ApplicationContext::getContainer()->get(RequestIdGenerator::class); + self::assertNotEquals($id, $generator->generate()); + }); + } +} diff --git a/src/HttpServer/tests/Cases/Middleware/CorsMiddlewareTest.php b/src/HttpServer/tests/Cases/Middleware/CorsMiddlewareTest.php new file mode 100644 index 00000000..1a429616 --- /dev/null +++ b/src/HttpServer/tests/Cases/Middleware/CorsMiddlewareTest.php @@ -0,0 +1,56 @@ +allows('getMethod')->andReturn('GET', 'OPTIONS'); + $handler = \Mockery::mock(RequestHandlerInterface::class); + $response = \Mockery::mock(ResponseInterface::class); + $handler->allows('handle')->andReturn($response); + $response->allows('withHeader') + ->andReturnUsing(function ($k, $v) use ($response) { + if ($k === 'Access-Control-Allow-Origin') { + $this->assertSame($v, '*'); + } + if ($k === 'Access-Control-Allow-Methods') { + $this->assertSame($v, 'GET,PUT,POST,DELETE,OPTIONS'); + } + if ($k === 'Access-Control-Allow-Credentials') { + $this->assertSame($v, 'true'); + } + if ($k === 'Access-Control-Allow-Headers') { + $this->assertSame($v, 'accept-language,authorization,lang,uid,token,Keep-Alive,User-Agent,Cache-Control,Content-Type'); + } + return $response; + }); + Context::set(ResponseInterface::class, $response); + $corsMiddleware->process($request, $handler); + $corsMiddleware->process($request, $handler); + } +} diff --git a/src/HttpServer/tests/Cases/Middleware/I18nMiddlewareTest.php b/src/HttpServer/tests/Cases/Middleware/I18nMiddlewareTest.php new file mode 100644 index 00000000..e74baa10 --- /dev/null +++ b/src/HttpServer/tests/Cases/Middleware/I18nMiddlewareTest.php @@ -0,0 +1,79 @@ + [ + LogLevel::DEBUG, + ], + 'translatable' => [ + 'locales' => [ + 'en', + 'zh' => [ + 'CN', + 'TW', + ], + ], + ], + ]); + ApplicationContext::getContainer() + ->set(ConfigInterface::class, $config); + ApplicationContext::getContainer()->define( + RequestIdGeneratorInterface::class, + RequestIdGenerator::class + ); + } + + public function testProcess(): void + { + $instance = ApplicationContext::getContainer()->get(I18nMiddleware::class); + $request = \Mockery::mock(ServerRequestInterface::class); + $translator = ApplicationContext::getContainer()->get(TranslatorInterface::class); + $request->allows('hasHeader') + ->andReturn(false, true, true, true, true); + $request->allows('getHeaderLine') + ->andReturn('en', 'zh_CN', 'zh_TW', 'test'); + $handler = \Mockery::mock(RequestHandlerInterface::class); + $handler->allows('handle')->andReturn(\Mockery::mock(ResponseInterface::class)); + $instance->process($request, $handler); + $this->assertSame($translator->getLocale(), 'zh_CN'); + $instance->process($request, $handler); + $this->assertSame($translator->getLocale(), 'en'); + $instance->process($request, $handler); + $this->assertSame($translator->getLocale(), 'zh_CN'); + $instance->process($request, $handler); + $this->assertSame($translator->getLocale(), 'zh_TW'); + } +} diff --git a/src/HttpServer/tests/Cases/Middleware/JsonMiddlewareTest.php b/src/HttpServer/tests/Cases/Middleware/JsonMiddlewareTest.php new file mode 100644 index 00000000..bce55b6a --- /dev/null +++ b/src/HttpServer/tests/Cases/Middleware/JsonMiddlewareTest.php @@ -0,0 +1,44 @@ +allows('withHeader') + ->withArgs(function ($key, $value) { + $this->assertSame($key, 'content-type'); + $this->assertSame($value, 'application/json; charset=utf-8'); + return true; + })->andReturn($response); + $handler = \Mockery::mock(RequestHandlerInterface::class); + $handler->allows('handle') + ->andReturn($response); + $request = \Mockery::mock(ServerRequestInterface::class); + $middleware->process($request, $handler); + } +} diff --git a/src/HttpServer/tests/Cases/Middleware/RequestIdMiddlewareTest.php b/src/HttpServer/tests/Cases/Middleware/RequestIdMiddlewareTest.php new file mode 100644 index 00000000..b6ff54e0 --- /dev/null +++ b/src/HttpServer/tests/Cases/Middleware/RequestIdMiddlewareTest.php @@ -0,0 +1,58 @@ +define( + RequestIdGeneratorInterface::class, + RequestIdGenerator::class + ); + } + + public function testProcess(): void + { + $middleware = new RequestIdMiddleware(); + $response = \Mockery::mock(ResponsePlusInterface::class); + $response + ->allows('withHeader') + ->andReturn($response); + $handler = \Mockery::mock(RequestHandlerInterface::class); + $handler->allows('handle') + ->andReturn($response); + $request = \Mockery::mock(ServerRequestInterface::class); + $middleware->process($request, $handler); + $this->assertSame( + Context::get(RequestIdGeneratorInterface::REQUEST_ID), + RequestIdHolder::getId() + ); + } +} diff --git a/src/HttpServer/tests/Cases/ResultTest.php b/src/HttpServer/tests/Cases/ResultTest.php new file mode 100644 index 00000000..733eb399 --- /dev/null +++ b/src/HttpServer/tests/Cases/ResultTest.php @@ -0,0 +1,112 @@ + [ + LogLevel::DEBUG, + ], + ]); + ApplicationContext::getContainer() + ->set(ConfigInterface::class, $config); + ApplicationContext::getContainer()->define( + RequestIdGeneratorInterface::class, + RequestIdGenerator::class + ); + } + + public function testConstruct(): void + { + $result = new Result( + true, + 'xxx', + HttpResultCode::SUCCESS, + ); + $this->assertInstanceOf(Result::class, $result); + $this->assertSame($result->message, 'xxx'); + $this->assertSame($result->getMessage(), 'xxx'); + $this->assertNull($result->getData()); + $this->assertNull($result->data); + $this->assertSame($result->getCode(), HttpResultCode::SUCCESS->value); + $this->assertSame($result->code, HttpResultCode::SUCCESS->value); + + $result->setMessage('xx2'); + $this->assertSame($result->message, 'xx2'); + $this->assertSame($result->getMessage(), 'xx2'); + + $result->setCode(HttpResultCode::FAILED); + $this->assertSame($result->getCode(), HttpResultCode::FAILED->value); + $this->assertSame($result->code, HttpResultCode::FAILED->value); + + $result->setData(['xxx']); + $this->assertSame($result->getData(), ['xxx']); + $this->assertSame($result->data, ['xxx']); + $this->assertSame($result->toArray(), [ + 'success' => true, + 'requestId' => RequestIdHolder::getId(), + 'message' => 'xx2', + 'data' => [ + 'xxx', + ], + 'code' => HttpResultCode::FAILED->value, + ]); + } + + public function testSuccess(): void + { + $result = Result::success( + 'xxx', + ['xxx'], + HttpResultCode::SUCCESS + ); + $this->assertSame($result->getMessage(), 'xxx'); + $this->assertSame($result->getData(), ['xxx']); + $this->assertTrue($result->isSuccess()); + $this->assertSame($result->data, ['xxx']); + $this->assertSame($result->getCode(), HttpResultCode::SUCCESS->value); + } + + public function testFailed(): void + { + $result = Result::error( + 'xxx', + ['xxx'], + HttpResultCode::FAILED + ); + $this->assertSame($result->getMessage(), 'xxx'); + $this->assertSame($result->getData(), ['xxx']); + $this->assertFalse($result->isSuccess()); + $this->assertSame($result->data, ['xxx']); + $this->assertSame($result->getCode(), HttpResultCode::FAILED->value); + } +} diff --git a/src/next-core-x/tests/Feature/Channel/RedisChannelTest.php b/src/next-core-x/tests/Feature/Channel/RedisChannelTest.php index 32d1e9a9..081c49e1 100644 --- a/src/next-core-x/tests/Feature/Channel/RedisChannelTest.php +++ b/src/next-core-x/tests/Feature/Channel/RedisChannelTest.php @@ -11,11 +11,14 @@ */ use Hyperf\Config\Config; use Hyperf\Context\ApplicationContext; +use Hyperf\Contract\ConfigInterface; use Hyperf\Redis\Redis; use Mine\NextCoreX\Channel\RedisChannel; use Mine\NextCoreX\Protocols\PhpSerialize; use Mine\NextCoreX\ReadConfig; +use function Hyperf\Support\env; + beforeEach(function () { $configInterface = new Config([ 'next-core-x' => [ @@ -24,8 +27,25 @@ ], ], 'serialize' => PhpSerialize::class, + 'redis' => [ + 'default' => [ + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'auth' => env('REDIS_AUTH', null), + 'port' => (int) env('REDIS_PORT', 6379), + 'db' => (int) env('REDIS_DB', 0), + 'pool' => [ + 'min_connections' => 1, + 'max_connections' => 10, + 'connect_timeout' => 10.0, + 'wait_timeout' => 3.0, + 'heartbeat' => -1, + 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60), + ], + ], + ], ]); $this->config = new ReadConfig($configInterface); + ApplicationContext::getContainer()->set(ConfigInterface::class, $configInterface); $redis = ApplicationContext::getContainer()->get(Redis::class); $this->channel = new RedisChannel($this->config, $redis); }); diff --git a/tests/Pest.php b/tests/Pest.php index 175a4173..e3e27647 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -11,14 +11,20 @@ */ use Hyperf\Context\ApplicationContext; use Hyperf\Contract\ConfigInterface; +use Hyperf\Contract\StdoutLoggerInterface; use Hyperf\Testing\Concerns\RunTestsInCoroutine; use Mine\Tests\TestCase; +use Psr\Log\LogLevel; uses(TestCase::class) ->beforeEach(function () { $mockConfig = Mockery::mock(ConfigInterface::class); $mockConfig->allows('has')->andReturn(true); - $mockConfig->allows('get')->andReturn([]); + $mockConfig->allows('get')->andReturn([ + StdoutLoggerInterface::class => [ + LogLevel::DEBUG, + ], + ]); $mockConfig->allows('set')->andReturn(true); ApplicationContext::getContainer() ->set(ConfigInterface::class, $mockConfig);