├── .github └── workflows │ ├── linux.yaml │ ├── macos.yaml │ └── windows.yaml ├── .gitignore ├── README-CN.md ├── README.md ├── pkgs.json └── src ├── .env.example ├── .gitignore ├── .gitlab-ci.yml ├── .php-cs-fixer.php ├── .phpstorm.meta.php ├── Dockerfile ├── app ├── Box.php ├── Command │ ├── AbstractCommand.php │ ├── AbstractPhpCallProxyCommand.php │ ├── BuildCommand.php │ ├── BuildPrepareCommand.php │ ├── BuildSelfCommand.php │ ├── ComposerProxyCommand.php │ ├── ConfigCommand.php │ ├── CsFixProxyCommand.php │ ├── DebugCommand.php │ ├── GetCommand.php │ ├── HyperfCommand.php │ ├── PhpCommand.php │ ├── PhpCsFixerProxyCommand.php │ ├── PhpstanProxyCommand.php │ ├── PhpunitProxyCommand.php │ ├── PintProxyCommand.php │ ├── ReverseProxyCommand.php │ ├── SelfUpdateCommand.php │ └── VersionCommand.php ├── Config.php ├── DownloadHandler │ ├── AbstractDownloadHandler.php │ ├── BoxHandler.php │ ├── ComposerHandler.php │ ├── DefaultHandler.php │ ├── MicroHandler.php │ ├── PhpHandler.php │ ├── PintHandler.php │ └── SwooleCliHandler.php ├── DownloadManager.php ├── Exception │ ├── BoxException.php │ ├── NotSupportVersionsException.php │ ├── PackageNotFoundException.php │ └── PkgDefinitionNotFoundException.php ├── GithubClient.php ├── PkgDefinition │ ├── Definition.php │ ├── Job.php │ ├── Jobs.php │ ├── Source.php │ └── Sources.php └── PkgDefinitionManager.php ├── bin └── hyperf.php ├── composer.json ├── config ├── autoload │ ├── annotations.php │ ├── dependencies.php │ ├── devtool.php │ └── logger.php ├── config.php └── container.php ├── deploy.test.yml ├── packages.json ├── phpstan.neon ├── phpunit.xml └── test ├── Cases └── ExampleTest.php ├── HttpTestCase.php └── bootstrap.php /.github/workflows/linux.yaml: -------------------------------------------------------------------------------- 1 | name: Linux build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | x86_64: 11 | name: PHP ${{ matrix.php-version }} ${{ matrix.arch }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | php-version: ["8.1"] 17 | arch: ["x86_64"] 18 | max-parallel: 4 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v2 24 | 25 | - name: Download PHP Cli 26 | id: php_cli 27 | run: | 28 | gh run download 5471614657 -R hyperf/lwmbs -n cli_static_${{ matrix.php-version }}_musl_${{ matrix.arch }}_1245a03ecd8971e80be6c5a88a947035278229272751ab84b382eafd735e9a2c 29 | ls -a 30 | chmod 755 ./php 31 | 32 | - name: Download Composer 33 | id: composer 34 | run: | 35 | wget https://getcomposer.org/download/2.4.4/composer.phar 36 | chmod 755 ./composer.phar 37 | 38 | - name: Vendor Installation 39 | id: vendor_installation 40 | run: | 41 | cd src 42 | ../php ../composer.phar install -o 43 | 44 | - name: Create ~/.box folder 45 | id: create_folder 46 | run: | 47 | cd ~ 48 | rm -rf .box 49 | mkdir .box 50 | chmod 755 .box 51 | 52 | - name: Build Box 53 | id: box 54 | continue-on-error: true 55 | run: | 56 | ./php src/bin/hyperf.php config set github.access-token ${{ env.GITHUB_TOKEN }} 57 | ./php src/bin/hyperf.php config set-php-version ${{ matrix.php-version }} 58 | ./php src/bin/hyperf.php build-prepare 59 | ./php src/bin/hyperf.php build-self --no-dev 60 | mv ~/.box/box ./box 61 | ./box version 62 | rm -rf ~/.box 63 | 64 | - name: Upload artifact for box 65 | if: steps.box.outcome == 'success' 66 | uses: actions/upload-artifact@v3 67 | with: 68 | name: box_${{ matrix.arch }} 69 | path: | 70 | box 71 | 72 | - name: Fail if box build failed 73 | if: steps.box.outcome != 'success' 74 | run: | 75 | false 76 | steps: 77 | 78 | - name: Get release 79 | id: get_release 80 | uses: bruceadams/get-release@v1.2.3 81 | continue-on-error: true 82 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | 86 | - name: Upload Release Asset 87 | if: steps.box.outcome == 'success' && steps.get_release.outputs.upload_url 88 | id: upload-release-asset 89 | uses: actions/upload-release-asset@v1 90 | env: 91 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 92 | with: 93 | upload_url: ${{ steps.get_release.outputs.upload_url }} 94 | asset_path: ./box 95 | asset_name: box_${{ matrix.arch }}_linux 96 | asset_content_type: application/x-sh 97 | 98 | aarch64: 99 | name: PHP ${{ matrix.php-version }} ${{ matrix.arch }} 100 | runs-on: [self-hosted, linux, arm64] 101 | strategy: 102 | fail-fast: false 103 | matrix: 104 | php-version: [ "8.1" ] 105 | arch: [ "aarch64" ] 106 | max-parallel: 4 107 | env: 108 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 109 | steps: 110 | - name: Checkout 111 | uses: actions/checkout@v2 112 | 113 | - name: Download PHP Cli 114 | id: php_cli 115 | run: | 116 | gh run download 5471614657 -R hyperf/lwmbs -n cli_static_${{ matrix.php-version }}_musl_${{ matrix.arch }}_1245a03ecd8971e80be6c5a88a947035278229272751ab84b382eafd735e9a2c 117 | ls -a 118 | chmod 755 ./php 119 | 120 | - name: Download Composer 121 | id: composer 122 | run: | 123 | wget https://getcomposer.org/download/2.4.4/composer.phar 124 | chmod 755 ./composer.phar 125 | 126 | - name: Vendor Installation 127 | id: vendor_installation 128 | run: | 129 | cd src 130 | ../php ../composer.phar install -o 131 | 132 | - name: Create ~/.box folder 133 | id: create_folder 134 | run: | 135 | cd ~ 136 | rm -rf ~/.box 137 | mkdir .box 138 | chmod 755 .box 139 | 140 | - name: Build Box 141 | id: box 142 | continue-on-error: true 143 | run: | 144 | ./php src/bin/hyperf.php config set github.access-token ${{ env.GITHUB_TOKEN }} 145 | ./php src/bin/hyperf.php config set-php-version ${{ matrix.php-version }} 146 | ./php src/bin/hyperf.php build-prepare 147 | ./php src/bin/hyperf.php build-self --no-dev 148 | mv ~/.box/box ./box 149 | ./box version 150 | rm -rf ~/.box 151 | 152 | - name: Upload artifact for box 153 | if: steps.box.outcome == 'success' 154 | uses: actions/upload-artifact@v3 155 | with: 156 | name: box_${{ matrix.arch }} 157 | path: | 158 | box 159 | 160 | - name: Fail if box build failed 161 | if: steps.box.outcome != 'success' 162 | run: | 163 | false 164 | steps: 165 | 166 | - name: Get release 167 | id: get_release 168 | uses: bruceadams/get-release@v1.2.3 169 | continue-on-error: true 170 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 171 | env: 172 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 173 | 174 | - name: Upload Release Asset 175 | if: steps.box.outcome == 'success' && steps.get_release.outputs.upload_url 176 | id: upload-release-asset 177 | uses: actions/upload-release-asset@v1 178 | env: 179 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 180 | with: 181 | upload_url: ${{ steps.get_release.outputs.upload_url }} 182 | asset_path: ./box 183 | asset_name: box_${{ matrix.arch }}_linux 184 | asset_content_type: application/x-sh 185 | -------------------------------------------------------------------------------- /.github/workflows/macos.yaml: -------------------------------------------------------------------------------- 1 | name: MacOS build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | macos: 11 | name: PHP ${{ matrix.php-version }} ${{ matrix.arch }} 12 | runs-on: macos-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | php-version: ["8.1"] 17 | arch: ["x86_64"] 18 | max-parallel: 4 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v2 24 | 25 | - name: Download PHP Cli 26 | id: php_cli 27 | run: | 28 | gh run download 6218236807 -R hyperf/lwmbs -n cli_${{ matrix.php-version }}_${{ matrix.arch }}_78ee8c86ed905c0569e22a6b2e2f46f25ec097c6ae2bfc53c8e790cf2d8c0b21 29 | chmod 755 ./php 30 | 31 | - name: Download Composer 32 | id: composer 33 | run: | 34 | wget https://getcomposer.org/download/2.3.7/composer.phar 35 | chmod 755 ./composer.phar 36 | 37 | - name: Vendor Installation 38 | id: vendor_installation 39 | run: | 40 | cd src 41 | ../php ../composer.phar install -o 42 | 43 | - name: Create ~/.box folder 44 | id: create_folder 45 | run: | 46 | cd ~ 47 | mkdir .box 48 | chmod 755 .box 49 | 50 | - name: Build Box 51 | id: box 52 | continue-on-error: true 53 | run: | 54 | ./php src/bin/hyperf.php config set github.access-token ${{ env.GITHUB_TOKEN }} 55 | ./php src/bin/hyperf.php config set-php-version ${{ matrix.php-version }} 56 | ./php src/bin/hyperf.php build-prepare 57 | ./php src/bin/hyperf.php build-self --no-dev 58 | mv ~/.box/box ./box 59 | 60 | - name: Upload artifact for box 61 | if: steps.box.outcome == 'success' 62 | uses: actions/upload-artifact@v3 63 | with: 64 | name: box_${{ matrix.arch }} 65 | path: | 66 | box 67 | 68 | - name: Fail if box build failed 69 | if: steps.box.outcome != 'success' 70 | run: | 71 | false 72 | 73 | - name: Get release 74 | id: get_release 75 | uses: bruceadams/get-release@v1.2.3 76 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 77 | continue-on-error: true 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | 81 | - name: Upload Release Asset 82 | if: steps.box.outcome == 'success' && steps.get_release.outputs.upload_url 83 | id: upload-release-asset 84 | uses: actions/upload-release-asset@v1 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | with: 88 | upload_url: ${{ steps.get_release.outputs.upload_url }} 89 | asset_path: ./box 90 | asset_name: box_${{ matrix.arch }}_macos 91 | asset_content_type: application/x-sh 92 | -------------------------------------------------------------------------------- /.github/workflows/windows.yaml: -------------------------------------------------------------------------------- 1 | name: Windows build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | x86_64: 11 | name: PHP ${{ matrix.php-version }} ${{ matrix.arch }} 12 | runs-on: windows-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | php-version: ["8.1"] 17 | arch: ["x64"] 18 | max-parallel: 4 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v2 24 | 25 | - name: Download PHP Cli 26 | id: php_cli 27 | run: | 28 | gh run download 6218251110 -R hyperf/lwmbs -n cli_${{ matrix.php-version }}_${{ matrix.arch }}_78ee8c86ed905c0569e22a6b2e2f46f25ec097c6ae2bfc53c8e790cf2d8c0b21 29 | ls -File 30 | 31 | - name: Download Composer 32 | id: composer 33 | run: | 34 | curl -o composer.phar https://getcomposer.org/download/2.3.7/composer.phar 35 | 36 | - name: Vendor Installation 37 | id: vendor_installation 38 | run: | 39 | cd src 40 | ../php ../composer.phar install -o 41 | 42 | - name: Create ~/.box folder 43 | id: create_folder 44 | run: | 45 | cd ~ 46 | mkdir .box 47 | 48 | - name: Build Box 49 | id: box 50 | continue-on-error: true 51 | run: | 52 | ./php src/bin/hyperf.php config set github.access-token ${{ env.GITHUB_TOKEN }} 53 | ./php src/bin/hyperf.php config set-php-version ${{ matrix.php-version }} 54 | ./php src/bin/hyperf.php build-prepare 55 | ./php src/bin/hyperf.php build-self --no-dev 56 | mv ~/.box/box.exe ./box.exe 57 | ./box.exe version 58 | 59 | - name: Upload artifact for box 60 | if: steps.box.outcome == 'success' 61 | uses: actions/upload-artifact@v3 62 | with: 63 | name: box_${{ matrix.arch }}.exe 64 | path: | 65 | box.exe 66 | 67 | - name: Fail if box build failed 68 | if: steps.box.outcome != 'success' 69 | run: | 70 | false 71 | steps: 72 | 73 | - name: Get release 74 | id: get_release 75 | uses: bruceadams/get-release@v1.2.3 76 | continue-on-error: true 77 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | 81 | - name: Upload Release Asset 82 | if: steps.box.outcome == 'success' && steps.get_release.outputs.upload_url 83 | id: upload-release-asset 84 | uses: actions/upload-release-asset@v1 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | with: 88 | upload_url: ${{ steps.get_release.outputs.upload_url }} 89 | asset_path: ./box.exe 90 | asset_name: box_${{ matrix.arch }}_windows.exe 91 | asset_content_type: application/x-sh 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .settings/ 3 | .project 4 | *.patch 5 | .idea/ 6 | .git/ 7 | runtime/ 8 | vendor/ 9 | .phpintel/ 10 | .env 11 | .DS_Store 12 | *.cache 13 | *.lock 14 | box 15 | .box 16 | *.phar 17 | .bashrc -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | [English](./README.md) | 中文 2 | 3 | # box, by Hyperf 4 | 5 | Box 致力于帮助提升 PHP 应用程序的编程体验,尤其有助于 Hyperf 应用,管理 PHP 环境和相关依赖,同时提供将 PHP 应用程序打包为二进制程序的能力,还提供反向代理服务来管理和部署 Swoole/Swow 服务。 6 | 7 | ## 目前还是早期实验版本,欢迎试玩 ~ 8 | 9 | ### 使用 10 | 11 | #### 安装 box 12 | 13 | ##### Mac 14 | 15 | ```bash 16 | wget https://github.com/hyperf/box/releases/download/v0.5.5/box_x86_64_macos -O box 17 | sudo mv ./box /usr/local/bin/box 18 | sudo chmod 755 /usr/local/bin/box 19 | // 确保 /usr/local/bin/box 在你的 $PATH 环境中,或者将 `box` 放到你想要的任意 $PATH 路径中 20 | ``` 21 | 22 | ##### Linux x86_64 23 | 24 | ```bash 25 | wget https://github.com/hyperf/box/releases/download/v0.5.5/box_x86_64_linux -O box 26 | sudo mv ./box /usr/local/bin/box 27 | sudo chmod 755 /usr/local/bin/box 28 | // 确保 /usr/local/bin/box 在你的 $PATH 环境中,或者将 `box` 放到你想要的任意 $PATH 路径中 29 | ``` 30 | ##### Linux aarch64 31 | 32 | 目前我们缺少 AARCH64 Github Actions Runner,所以无法及时构建 AARCH64 版本的 bin 文件。 33 | 34 | ```bash 35 | wget https://github.com/hyperf/box/releases/download/v0.0.3/box_php8.1_aarch64_linux -O box 36 | sudo mv ./box /usr/local/bin/box 37 | sudo chmod 755 /usr/local/bin/box 38 | // 确保 /usr/local/bin/box 在你的 $PATH 环境中,或者将 `box` 放到你想要的任意 $PATH 路径中 39 | ``` 40 | 41 | ##### Windows 42 | 43 | ```powershell 44 | curl -o box.exe https://github.com/hyperf/box/releases/download/v0.5.5/box_x64_windows.exe 45 | // 将 `box.exe` 放到你想要的任意 Path 环境变量路径中,同时 Windows 版本在执行时需要在命令行中使用 `box.exe` 而不是 `box` 46 | ``` 47 | 48 | #### 初始化 Github Access Token 49 | 50 | Box 需要一个 Github 访问令牌来请求 Github API,以检索包的版本。 51 | 52 | 1. [创建 Github Access Token](https://github.com/settings/tokens/new?scopes=workflow,repo&description=HyperfBox),`workflow` 范围需要勾选; 53 | 2. 运行 `box config set github.access-token ` 命令来设置您的 token; 54 | 3. 玩得开心 ~ 55 | 56 | #### 设置 Box Kernel 57 | 58 | 默认情况下,Box 由 Swow Kernel 提供支持,但是我们也提供了 Swoole Kernel,您可以通过 `box config set kernel swoole` 来切换为 Swoole Kernel,但是需要注意的是,Swoole Kernel 仅支持 PHP 8.1 版本,且不支持构建二进制程序功能和 Windows 系统环境。 59 | 60 | ```bash 61 | // 设置为 Swow Kernel [默认] 62 | box config set kernel swow 63 | 64 | // 设置为 Swoole Kernel (不支持 Windows) 65 | box config set kernel swoole 66 | ``` 67 | 68 | ### 命令 69 | 70 | - `box get pkg@version`从远程安装包,`pkg`是包名,`version`是包的版本,`box get pkg`表示安装最新版本的 pkg,例如, 运行 `box get php@8.1` 安装 PHP 8.1, 运行 `box get composer` 安装最新的 composer bin 71 | - `box build-prepare` 为 `build` 和 `build-self` 命令做好相关环境的准备 72 | - `box build-self` 构建 `box` bin 本身 73 | - `box build ` 将 Hyperf 应用程序构建成二进制文件 74 | - `box self-update` 将 `box` bin 更新至最新版本 75 | - `box config list` 输出 box 配置文件的所有内容 76 | - `box config get ` 从配置文件中按键检索值 77 | - `box config set `通过 key 设置 value 到配置文件中 78 | - `box config unset `按 key 删除配置值 79 | - `box config set-php-version `设置 box 的当前 PHP 版本,可用值:8.0 | 8.1 80 | - `box config get-php-version `获取 box 的当前设置的 PHP 版本 81 | - `box reverse-proxy -u ` 启动一个反向代理 HTTP 服务器,用于将 HTTP 请求转发到指定的多个上游服务器 82 | - `box php ` 通过当前 box 的 PHP 版本运行任何 PHP 命令 83 | - `box composer `通过当前 box 的 PHP 版本运行任何 Composer 命令,composer bin 的版本取决于最后执行的`get composer`命令 84 | - `box php-cs-fixer ` 通过当前 box 的 PHP 版本运行任何 `php-cs-fixer` 命令,composer bin 的版本取决于最后执行的 `get php-cs-fixer` 命令 85 | - `box cs-fix ` 通过当前 box 的 PHP 版本运行 `php-cs-fixer fix` 命令,composer bin 的版本取决于最后执行的 `get php-cs-fixer` 命令 86 | - `box phpstan ` 通过当前 box 的 PHP 版本运行任何 `phpstan` 命令,composer bin 的版本取决于最后执行的 `get phpstan` 命令,此命令仅在 box v0.3.0 及以上的版本中可用 87 | - `box pint ` 通过当前 box 的 PHP 版本运行任何 `pint` 命令,composer bin 的版本取决于最后执行的 `get pint` 命令,此命令仅在 box v0.3.0 及以上的版本中可用 88 | - `box version` 输出当前 box bin 的版本号 89 | 90 | ### 关于 Swow-Skeleton 91 | 92 | 希望体验 Box 完整功能的朋友,需要通过 Swow Kernel 来运行,因此您需要基于 [hyperf/swow-skeleton](https://github.com/hyperf/swow-skeleton) 来运行您的项目,可通过 `box composer create-project hyperf/swow-skeleton:dev-master` 命令来创建一个基于 Hyperf 3.0 RC 版的 Swow 骨架项目。 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [中文](./README-CN.md) 2 | 3 | # box, by Hyperf 4 | 5 | Box is committed to helping improve the programming experience of PHP applications, expecially for Hyperf, managing the PHP environment and related dependencies, providing the ability to package PHP applications as binary programs, and also providing reverse proxy services for managing and deploying Swoole/Swow applications. 6 | 7 | ## This is still an early experimental version, have fun ~ 8 | 9 | ### Usage 10 | 11 | #### Install box 12 | 13 | ##### Mac 14 | 15 | ```bash 16 | wget https://github.com/hyperf/box/releases/download/v0.5.5/box_x86_64_macos -O box 17 | sudo mv ./box /usr/local/bin/box 18 | sudo chmod 755 /usr/local/bin/box 19 | // Make sure /usr/local/bin/box in your $PATH env, or put `box` into any path in $PATH env that you want 20 | ``` 21 | 22 | ##### Linux x86_64 23 | 24 | ```bash 25 | wget https://github.com/hyperf/box/releases/download/v0.5.5/box_x86_64_linux -O box 26 | sudo mv ./box /usr/local/bin/box 27 | sudo chmod 755 /usr/local/bin/box 28 | // Make sure /usr/local/bin/box in your $PATH env, or put `box` into any path in $PATH env that you want 29 | ``` 30 | ##### Linux aarch64 31 | 32 | At present, we are short of AARCH64 Github Actions Runner, so we cannot timely construct the bin file of AARCH64 version. 33 | 34 | ```bash 35 | wget https://github.com/hyperf/box/releases/download/v0.0.3/box_php8.1_aarch64_linux -O box 36 | sudo mv ./box /usr/local/bin/box 37 | sudo chmod 755 /usr/local/bin/box 38 | // Make sure /usr/local/bin/box in your $PATH env, or put `box` into any path in $PATH env that you want 39 | ``` 40 | 41 | ##### Windows 42 | 43 | ```powershell 44 | curl -o box.exe https://github.com/hyperf/box/releases/download/v0.5.5/box_x64_windows.exe 45 | // Put `box.exe` into any path in $PATH env that you want, and use `box.exe` instead of `box` when executing on Windows 46 | ``` 47 | 48 | #### Init Github Access Token 49 | 50 | Box needs a Github Access Token to request github api, to retrieve the versions of the package. 51 | 52 | 1. [Create Github Access Token](https://github.com/settings/tokens/new?scopes=workflow,repo&description=HyperfBox), the `workflow` scope have to be selected. 53 | 2. Run `box config set github.access-token ` to init the token. 54 | 3. Have fun ~ 55 | 56 | #### Setting the Box Kernel 57 | 58 | By default, Box is supported by Swow Kernel, but we also provide Swoole Kernel, you can switch to Swoole Kernel by `box config set kernel swoole`, but it should be noted that Swoole Kernel only supports PHP 8.1 version, and The Build Binaries feature and Windows Systems are not supported. 59 | 60 | ```bash 61 | // set to Swow Kernel [default] 62 | box config set kernel swow 63 | 64 | // set to Swoole Kernel (NOT supported on Windows) 65 | box config set kernel swoole 66 | ```` 67 | 68 | ### Commands 69 | 70 | - `box get pkg@version` to install the package from remote automatically, `pkg` is the package name, and `version` is the version of package, `box get pkg` means to install the latest version of pkg, for example, run `box get php@8.1` to install the PHP 8.1, run `box get composer` to install the latest composer bin 71 | - `box build-prepare` to get ready for `build` and `build-self` command 72 | - `box build-self` to build the `box` bin itself 73 | - `box build ` to build a Hyperf application into a binary file 74 | - `box self-update` to update the `box` bin to latest version 75 | - `box config list` to dump the config file 76 | - `box config get ` to retrieve the value by key from config file 77 | - `box config set ` to set value by key into the config file 78 | - `box config unset ` to unset the config value by key 79 | - `box config set-php-version ` to set the current PHP version of box, available value: 8.0 | 8.1 80 | - `box config get-php-version ` to get the current PHP version of box 81 | - `box reverse-proxy -u ` to start a reverse proxy HTTP server for the upstream servers 82 | - `box php ` to run any PHP command via current PHP version of box 83 | - `box composer ` to run any Composer command via box, the version of the composer bin depends on the last executed `get composer` command 84 | - `box php-cs-fixer ` to run any `php-cs-fixer` command via box, the version of the composer bin depends on the last executed `get php-cs-fixer` command 85 | - `box cs-fix ` to run `php-cs-fix fix` command via box, the version of the composer bin depends on the last executed `get php-cs-fixer` command 86 | - `box phpstan ` to run any `phpstan` command via box, the version of the composer bin depends on the last executed `get phpstan` command, since box v0.3.0 87 | - `box pint ` to run any `pint` command via box, the version of the composer bin depends on the last executed `get pint` command, since box v0.3.0 88 | - `box version` to dump the current version of the box bin 89 | 90 | ### About Swow Skeleton 91 | 92 | If you want to experience the full features of Box, you need to run it based on the Swow Kernel, so you need to base your project on [hyperf/swow-skeleton](https://github.com/hyperf/swow-skeleton) to run your project, you can create a Swow skeleton project based on Hyperf 3.0 RC version by `box composer create-project hyperf/swow-skeleton:dev-master` command. 93 | -------------------------------------------------------------------------------- /pkgs.json: -------------------------------------------------------------------------------- 1 | { 2 | "php-cs-fixer": { 3 | "repo": "FriendsOfPHP/PHP-CS-Fixer", 4 | "bin": "php-cs-fixer.phar" 5 | }, 6 | "phpunit": { 7 | "url": "https://phar.phpunit.de/phpunit-${{version}}.phar", 8 | "bin": "phpunit.phar", 9 | "composer_name": "phpunit/phpunit", 10 | "latest_fetch_type": "packagist" 11 | }, 12 | "phpstan": { 13 | "repo": "phpstan/phpstan", 14 | "bin": "phpstan.phar" 15 | }, 16 | "php": { 17 | "repo": "hyperf/lwmbs", 18 | "jobs": { 19 | "Darwin.x86_64": "11339720662", 20 | "Darwin.arm64": "11339720662", 21 | "Linux.x86_64": "11339726597", 22 | "Linux.aarch64": "11339726597", 23 | "Windows.AMD64": "11267510778" 24 | }, 25 | "job_artifact_match_rule": { 26 | "Darwin.x86_64": "${{prefix}}_max-swow_${{php-version}}_${{arch}}", 27 | "Darwin.arm64": "${{prefix}}_max-swow_${{php-version}}_${{arch}}", 28 | "Linux.x86_64": "${{prefix}}_static_max-swow_${{php-version}}_musl_${{arch}}", 29 | "Linux.aarch64": "${{prefix}}_static_max-swow_${{php-version}}_musl_${{arch}}", 30 | "Windows.AMD64": "${{prefix}}_max-swow_${{php-version}}_x64" 31 | }, 32 | "latest": "8.3", 33 | "versions": ["8.3" ,"8.2", "8.1", "8.0"] 34 | }, 35 | "micro": { 36 | "repo": "hyperf/lwmbs", 37 | "jobs": { 38 | "Darwin.x86_64": "11339720662", 39 | "Darwin.arm64": "11339720662", 40 | "Linux.x86_64": "11339726597", 41 | "Linux.aarch64": "11339726597", 42 | "Windows.AMD64": "11267510778" 43 | }, 44 | "job_artifact_match_rule": { 45 | "Darwin.x86_64": "${{prefix}}_max-swow_${{php-version}}_${{arch}}", 46 | "Darwin.arm64": "${{prefix}}_max-swow_${{php-version}}_${{arch}}", 47 | "Linux.x86_64": "${{prefix}}_static_max-swow_${{php-version}}_musl_${{arch}}", 48 | "Linux.aarch64": "${{prefix}}_static_max-swow_${{php-version}}_musl_${{arch}}", 49 | "Windows.AMD64": "${{prefix}}_max-swow_${{php-version}}_x64" 50 | }, 51 | "latest": "8.3", 52 | "versions": ["8.3" ,"8.2", "8.1", "8.0"] 53 | }, 54 | "pint": { 55 | "url": "https://github.com/laravel/pint/archive/refs/tags/${{version}}.zip", 56 | "bin": "pint", 57 | "composer_name": "laravel/pint", 58 | "latest_fetch_type": "packagist" 59 | }, 60 | "box": { 61 | "repo": "hyperf/box", 62 | "bin": "box", 63 | "release_asset_keyword": "box", 64 | "release_asset_match_rule": { 65 | "Darwin.x86_64": "box_x86_64_macos", 66 | "Darwin.arm64": "box_arm64_macos", 67 | "Linux.x86_64": "box_x86_64_linux", 68 | "Linux.aarch64": "box_aarch64_linux" 69 | } 70 | }, 71 | "composer": { 72 | "repo": "composer/composer", 73 | "bin": "composer.phar", 74 | "sources": { 75 | "github.com": { 76 | "type": "github", 77 | "url": "github.com" 78 | }, 79 | "getcomposer.org": { 80 | "type": "url", 81 | "url": "https://getcomposer.org/download/${{version}}/${{bin}}" 82 | }, 83 | "default": { 84 | "type": "url", 85 | "url": "https://getcomposer.org/download/${{version}}/${{bin}}" 86 | } 87 | }, 88 | "latest": "latest", 89 | "latest_fetch_type": "github" 90 | }, 91 | "swoole-cli": { 92 | "repo": "swoole/swoole-src", 93 | "bin": "swoole-cli", 94 | "latest": "v5.1.2", 95 | "release_asset_keyword": "swoole-cli", 96 | "release_asset_match_rule": { 97 | "Linux": "swoole-cli-${{version}}-linux-x64.tar.xz", 98 | "Darwin": "swoole-cli-${{version}}-macos-x64.tar.xz" 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=swow-skeleton 2 | 3 | # Mysql 4 | DB_DRIVER=mysql 5 | DB_HOST=127.0.0.1 6 | DB_PORT=3306 7 | DB_DATABASE=hyperf 8 | DB_USERNAME=root 9 | DB_PASSWORD= 10 | DB_CHARSET=utf8mb4 11 | DB_COLLATION=utf8mb4_unicode_ci 12 | DB_PREFIX= 13 | 14 | # Redis 15 | REDIS_HOST=127.0.0.1 16 | REDIS_AUTH=(null) 17 | REDIS_PORT=6379 18 | REDIS_DB=0 19 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .settings/ 3 | .project 4 | *.patch 5 | .idea/ 6 | .git/ 7 | runtime/ 8 | vendor/ 9 | .phpintel/ 10 | .env 11 | .DS_Store 12 | *.cache 13 | *.lock 14 | box 15 | .box 16 | *.phar -------------------------------------------------------------------------------- /src/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # usermod -aG docker gitlab-runner 2 | 3 | stages: 4 | - build 5 | - deploy 6 | 7 | variables: 8 | PROJECT_NAME: hyperf 9 | REGISTRY_URL: registry-docker.org 10 | 11 | build_test_docker: 12 | stage: build 13 | before_script: 14 | # - git submodule sync --recursive 15 | # - git submodule update --init --recursive 16 | script: 17 | - docker build . -t $PROJECT_NAME 18 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:test 19 | - docker push $REGISTRY_URL/$PROJECT_NAME:test 20 | only: 21 | - test 22 | tags: 23 | - builder 24 | 25 | deploy_test_docker: 26 | stage: deploy 27 | script: 28 | - docker stack deploy -c deploy.test.yml --with-registry-auth $PROJECT_NAME 29 | only: 30 | - test 31 | tags: 32 | - test 33 | 34 | build_docker: 35 | stage: build 36 | before_script: 37 | # - git submodule sync --recursive 38 | # - git submodule update --init --recursive 39 | script: 40 | - docker build . -t $PROJECT_NAME 41 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME 42 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:latest 43 | - docker push $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME 44 | - docker push $REGISTRY_URL/$PROJECT_NAME:latest 45 | only: 46 | - tags 47 | tags: 48 | - builder 49 | 50 | deploy_docker: 51 | stage: deploy 52 | script: 53 | - echo SUCCESS 54 | only: 55 | - tags 56 | tags: 57 | - builder 58 | -------------------------------------------------------------------------------- /src/.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | '@Symfony' => true, 17 | '@DoctrineAnnotation' => true, 18 | '@PhpCsFixer' => true, 19 | 'header_comment' => [ 20 | 'comment_type' => 'PHPDoc', 21 | 'header' => $header, 22 | 'separate' => 'none', 23 | 'location' => 'after_declare_strict', 24 | ], 25 | 'array_syntax' => [ 26 | 'syntax' => 'short' 27 | ], 28 | 'list_syntax' => [ 29 | 'syntax' => 'short' 30 | ], 31 | 'concat_space' => [ 32 | 'spacing' => 'one' 33 | ], 34 | 'blank_line_before_statement' => [ 35 | 'statements' => [ 36 | 'declare', 37 | ], 38 | ], 39 | 'general_phpdoc_annotation_remove' => [ 40 | 'annotations' => [ 41 | 'author' 42 | ], 43 | ], 44 | 'ordered_imports' => [ 45 | 'imports_order' => [ 46 | 'class', 'function', 'const', 47 | ], 48 | 'sort_algorithm' => 'alpha', 49 | ], 50 | 'single_line_comment_style' => [ 51 | 'comment_types' => [ 52 | ], 53 | ], 54 | 'yoda_style' => [ 55 | 'always_move_variable' => false, 56 | 'equal' => false, 57 | 'identical' => false, 58 | ], 59 | 'phpdoc_align' => [ 60 | 'align' => 'left', 61 | ], 62 | 'multiline_whitespace_before_semicolons' => [ 63 | 'strategy' => 'no_multi_line', 64 | ], 65 | 'constant_case' => [ 66 | 'case' => 'lower', 67 | ], 68 | 'class_attributes_separation' => true, 69 | 'combine_consecutive_unsets' => true, 70 | 'declare_strict_types' => true, 71 | 'linebreak_after_opening_tag' => true, 72 | 'lowercase_static_reference' => true, 73 | 'no_useless_else' => true, 74 | 'no_unused_imports' => true, 75 | 'not_operator_with_successor_space' => true, 76 | 'not_operator_with_space' => false, 77 | 'ordered_class_elements' => true, 78 | 'php_unit_strict' => false, 79 | 'phpdoc_separation' => false, 80 | 'single_quote' => true, 81 | 'standardize_not_equals' => true, 82 | 'multiline_comment_opening_closing' => true, 83 | ]) 84 | ->setFinder( 85 | PhpCsFixer\Finder::create() 86 | ->exclude('public') 87 | ->exclude('runtime') 88 | ->exclude('vendor') 89 | ->in(__DIR__) 90 | ) 91 | ->setUsingCache(false); 92 | -------------------------------------------------------------------------------- /src/.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | " version="1.0" license="MIT" app.name="Hyperf" 10 | 11 | ## 12 | # ---------- env settings ---------- 13 | ## 14 | # --build-arg timezone=Asia/Shanghai 15 | ARG timezone 16 | 17 | ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \ 18 | APP_ENV=prod \ 19 | SCAN_CACHEABLE=(true) 20 | 21 | # update 22 | RUN set -ex \ 23 | # show php version and extensions 24 | && php -v \ 25 | && php -m \ 26 | && php --ri swow \ 27 | # ---------- some config ---------- 28 | && cd /etc/php8 \ 29 | # - config PHP 30 | && { \ 31 | echo "upload_max_filesize=128M"; \ 32 | echo "post_max_size=128M"; \ 33 | echo "memory_limit=1G"; \ 34 | echo "date.timezone=${TIMEZONE}"; \ 35 | } | tee conf.d/99_overrides.ini \ 36 | # - config timezone 37 | && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ 38 | && echo "${TIMEZONE}" > /etc/timezone \ 39 | # ---------- clear works ---------- 40 | && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \ 41 | && echo -e "\033[42;37m Build Completed :).\033[0m\n" 42 | 43 | WORKDIR /opt/www 44 | 45 | # Composer Cache 46 | # COPY ./composer.* /opt/www/ 47 | # RUN composer install --no-dev --no-scripts 48 | 49 | COPY . /opt/www 50 | RUN composer install --no-dev -o && php bin/hyperf.php 51 | 52 | EXPOSE 9501 53 | 54 | ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"] 55 | -------------------------------------------------------------------------------- /src/app/Box.php: -------------------------------------------------------------------------------- 1 | isFunctionExists('passthru')) { 33 | passthru($command); 34 | } elseif ($this->isFunctionExists('proc_open')) { 35 | proc_open($command, [ 36 | 0 => STDIN, 37 | 1 => STDOUT, 38 | 2 => STDERR 39 | ], $pipes); 40 | } elseif ($this->isFunctionExists(['popen', 'pclose', 'feof', 'fgets'])) { 41 | $handle = popen($command, 'r'); 42 | while (! feof($handle)) { 43 | $data = fgets($handle); 44 | echo $data; 45 | } 46 | pclose($handle); 47 | } else { 48 | throw new RuntimeException('No available function to run command.'); 49 | } 50 | } 51 | 52 | protected function getRuntimePath(): string 53 | { 54 | $base = getenv('HOME') ?: getenv('USERPROFILE'); 55 | return $this->config->getConfig('path.runtime', $base . DIRECTORY_SEPARATOR . '.box'); 56 | } 57 | 58 | protected function getCurrentPhpVersion(): string 59 | { 60 | return $this->config->getConfig('versions.php', '8.1'); 61 | } 62 | 63 | protected function isFunctionExists(string|array $functions): bool 64 | { 65 | $isExists = true; 66 | foreach ((array) $functions as $function) { 67 | if (! function_exists($function)) { 68 | $isExists = false; 69 | break; 70 | } 71 | } 72 | return $isExists; 73 | } 74 | 75 | protected function buildBinPath(): string 76 | { 77 | $path = $this->getRuntimePath(); 78 | $kernel = strtolower($this->config->getConfig('kernel', 'swow')); 79 | $currentPhpVersion = $this->getCurrentPhpVersion(); 80 | $os = PHP_OS_FAMILY; 81 | if ($kernel === 'swoole') { 82 | if ($os === 'Windows') { 83 | throw new BoxException('The command is not supported in Swoole kernel on Windows.'); 84 | } 85 | if ($currentPhpVersion < '8.1') { 86 | $this->logger->warning(sprintf('Current setting PHP version is %s, but the kernel is Swoole and Swoole only support 8.1, so the PHP version is forced to 8.1.', $currentPhpVersion)); 87 | } 88 | $bin = $path . DIRECTORY_SEPARATOR . 'swoole-cli'; 89 | } else { 90 | $extension = ''; 91 | if ($os === 'Windows') { 92 | $extension = '.exe'; 93 | } 94 | $bin = $path . DIRECTORY_SEPARATOR . 'php' . $currentPhpVersion . $extension; 95 | } 96 | return $bin; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/app/Command/AbstractPhpCallProxyCommand.php: -------------------------------------------------------------------------------- 1 | setDescription(sprintf('The proxy command of `php %s`.', $this->proxyCommand)); 27 | $this->setName($this->proxyCommand); 28 | $this->ignoreValidationErrors(); 29 | } 30 | 31 | public function handle() 32 | { 33 | $bin = $this->buildBinCommand(); 34 | $command = Str::replaceFirst($this->proxyCommand, '', (string) $this->input); 35 | $fullCommand = sprintf('%s %s', $bin, trim($command)); 36 | $this->liveCommand($fullCommand); 37 | } 38 | 39 | protected function buildBinCommand(): string 40 | { 41 | $bin = $this->buildBinPath(); 42 | return $bin . ' ' . $this->getRuntimePath() . DIRECTORY_SEPARATOR . $this->proxyBin; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/Command/BuildCommand.php: -------------------------------------------------------------------------------- 1 | setName('build'); 26 | $this->setDescription('Build the application as a bin.'); 27 | $this->addArgument('path', InputArgument::OPTIONAL, 'The path wants to build.', '.' . DIRECTORY_SEPARATOR); 28 | $this->addOption('name', '', InputOption::VALUE_OPTIONAL, 'The name of the output bin.', 'hyperf'); 29 | $this->addOption('output', 'o', InputOption::VALUE_OPTIONAL, 'The output path of bin.', '.'); 30 | $this->addOption('dev', 'd', InputOption::VALUE_NEGATABLE, 'Require the dev composer packages or not.', true); 31 | $this->addOption('ini-path', 'i', InputOption::VALUE_OPTIONAL, 'The path for php.ini.'); 32 | } 33 | 34 | public function handle() 35 | { 36 | $kernel = strtolower($this->config->getConfig('kernel', 'swow')); 37 | if ($kernel === 'swoole') { 38 | $this->logger->error('The build command is not supported in Swoole kernel.'); 39 | return; 40 | } 41 | $path = $this->input->getArgument('path'); 42 | $binName = $this->input->getOption('name'); 43 | $outputPath = $this->input->getOption('output'); 44 | $iniPath = $this->input->getOption('ini-path'); 45 | $runtimePath = $this->getRuntimePath(); 46 | $currentPhpVersion = $this->getCurrentPhpVersion(); 47 | $extension = ''; 48 | if (PHP_OS_FAMILY === 'Windows') { 49 | $extension = '.exe'; 50 | } 51 | $composer = $runtimePath . DIRECTORY_SEPARATOR . 'composer.phar'; 52 | $php = $runtimePath . DIRECTORY_SEPARATOR . 'php' . $currentPhpVersion . $extension; 53 | $micro = $runtimePath . DIRECTORY_SEPARATOR . 'micro_php' . $currentPhpVersion . '.sfx'; 54 | if (! file_exists($composer) || ! file_exists($php) || ! file_exists($micro)) { 55 | $this->output->error('The build environment is broken, run `box build-prepare` command to make it ready.'); 56 | return static::FAILURE; 57 | } 58 | $outputBin = $outputPath . DIRECTORY_SEPARATOR . $binName . $extension; 59 | $handledOutputBin = $outputBin; 60 | // If $outputBin is a relative path, convert it to an absolute path, note that it is adapted to the Windows environment 61 | if (!str_starts_with($outputBin, DIRECTORY_SEPARATOR) && strpos($outputBin, ':') !== 1) { 62 | $handledOutputBin = getcwd() . DIRECTORY_SEPARATOR . $outputBin; 63 | } 64 | $this->buildINICommand($iniPath, $path); 65 | $composerNoDevCmd = $this->buildComposerNoDevCommand($php, $composer); 66 | $command = '%s -d phar.readonly=Off .\bin\hyperf.php phar:build --name=box-build.phar.tmp && '; 67 | if (PHP_OS_FAMILY === 'Windows') { 68 | $command .= 'COPY /b %s + .\ini.tmp + .\box-build.phar.tmp %s /y && DEL .\box-build.phar.tmp .\ini.tmp'; 69 | } else { 70 | $command .= 'cat %s ./ini.tmp ./box-build.phar.tmp > %s && rm -rf ./box-build.phar.tmp ./ini.tmp'; 71 | } 72 | $fullCommand = sprintf( 73 | 'cd %s && ' . $composerNoDevCmd . $command, 74 | $path, 75 | $php, 76 | $micro, 77 | $handledOutputBin 78 | ); 79 | 80 | $this->liveCommand($fullCommand); 81 | if (file_exists($handledOutputBin)) { 82 | $this->output->success(sprintf('The application %s is built successfully.', $outputBin)); 83 | chmod($handledOutputBin, 0755); 84 | } else { 85 | $this->output->error(sprintf('The application %s is built failed.', $outputBin)); 86 | } 87 | } 88 | 89 | protected function buildComposerNoDevCommand(string $php, string $composer): string 90 | { 91 | $devMode = $this->input->getOption('dev'); 92 | $composerNoDevCmd = ''; 93 | if (! $devMode) { 94 | $composerNoDevCmd = sprintf('%s %s install -o --no-dev && ', $php, $composer); 95 | } 96 | return $composerNoDevCmd; 97 | } 98 | 99 | protected function buildINICommand(?string $iniPath, string $path): string 100 | { 101 | $ini = ''; 102 | if ($iniPath && file_exists($iniPath) && is_file($iniPath)) { 103 | $ini = file_get_contents($iniPath); 104 | } 105 | $ini = "\n$ini\n"; 106 | 107 | $f = fopen($path . DIRECTORY_SEPARATOR . 'ini.tmp', 'wb'); 108 | fwrite($f, "\xfd\xf6\x69\xe6"); 109 | fwrite($f, pack("N", strlen($ini))); 110 | fwrite($f, $ini); 111 | fclose($f); 112 | return 'ini.tmp'; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/app/Command/BuildPrepareCommand.php: -------------------------------------------------------------------------------- 1 | config = $container->get(Config::class); 31 | } 32 | 33 | public function configure() 34 | { 35 | parent::configure(); 36 | $this->setDescription('Prepare the enviroments for build the box cli.'); 37 | $this->addOption('refresh', 'r', InputOption::VALUE_OPTIONAL, '', false); 38 | } 39 | 40 | public function handle() 41 | { 42 | $refresh = $this->input->getOption('refresh'); 43 | $refresh = $refresh !== false; 44 | $runtimePath = $this->config->getConfig('path.runtime', getenv('HOME') . '/.box'); 45 | $currentPhpVersion = $this->config->getConfig('versions.php', '8.1'); 46 | $composer = $runtimePath . '/composer.phar'; 47 | $php = $runtimePath . '/php' . $currentPhpVersion; 48 | $micro = $runtimePath . '/micro_php' . $currentPhpVersion . '.sfx'; 49 | $getCommand = $this->getApplication()->find('get'); 50 | if (! file_exists($composer) || $refresh) { 51 | $getCommand->run(new ArrayInput(['pkg' => 'composer']), $this->output); 52 | } 53 | if (! file_exists($php) || $refresh) { 54 | $getCommand->run(new ArrayInput(['pkg' => 'php@' . $currentPhpVersion]), $this->output); 55 | } 56 | if (! file_exists($micro) || $refresh) { 57 | $getCommand->run(new ArrayInput(['pkg' => 'micro@' . $currentPhpVersion]), $this->output); 58 | } 59 | 60 | $this->output->info('It is ready to build, try it now.'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/Command/BuildSelfCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Build the application as a bin.'); 31 | $this->addArgument('path', InputArgument::OPTIONAL, 'The path wants to build.', './src'); 32 | $this->addOption('name', '', InputOption::VALUE_OPTIONAL, 'The name of the output bin.', 'box'); 33 | $defaultOutputPath = $this->config->getConfig('path.bin', getenv('HOME') . '/.box'); 34 | $this->addOption('output', 'o', InputOption::VALUE_OPTIONAL, 'The output path of bin.', $defaultOutputPath); 35 | $this->addOption('dev', 'd', InputOption::VALUE_NEGATABLE, 'Require the dev composer packages or not.', true); 36 | } 37 | 38 | protected function buildComposerNoDevCommand(string $php, string $composer): string 39 | { 40 | $devMode = $this->input->getOption('dev'); 41 | $composerNoDevCmd = ''; 42 | if (! $devMode) { 43 | $composerNoDevCmd = sprintf('%s %s update -o --no-dev && ', $php, $composer); 44 | } 45 | return $composerNoDevCmd; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/Command/ComposerProxyCommand.php: -------------------------------------------------------------------------------- 1 | config = $this->container->get(Config::class); 30 | } 31 | 32 | public function configure() 33 | { 34 | parent::configure(); 35 | $this->setDescription('Config of box.'); 36 | $this->addArgument('action', InputArgument::REQUIRED, ''); 37 | $this->addArgument('arg1', InputArgument::OPTIONAL, ''); 38 | $this->addArgument('arg2', InputArgument::OPTIONAL, ''); 39 | } 40 | 41 | public function handle() 42 | { 43 | $action = $this->input->getArgument('action'); 44 | switch ($action) { 45 | case 'list': 46 | $this->output->writeln(json_encode($this->config->getConfigContent(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); 47 | break; 48 | case 'unset': 49 | $key = $this->input->getArgument('arg1'); 50 | $this->config->updateConfig($key, null); 51 | break; 52 | case 'set': 53 | $key = $this->input->getArgument('arg1'); 54 | $value = $this->input->getArgument('arg2'); 55 | $this->config->updateConfig($key, $value); 56 | break; 57 | case 'set-php-version': 58 | $value = $this->input->getArgument('arg1'); 59 | $this->config->updateConfig('versions.php', $value); 60 | break; 61 | case 'get-php-version': 62 | $key = 'versions.php'; 63 | $value = $this->config->getConfig($key); 64 | $this->output->block([ 65 | sprintf('%s: %s', $key, $value), 66 | ]); 67 | break; 68 | case 'get': 69 | $key = $this->input->getArgument('arg1'); 70 | $value = $this->config->getConfig($key); 71 | if (is_bool($value)) { 72 | $value = $value ? 'true' : 'false'; 73 | } 74 | if (is_null($value)) { 75 | $value = 'null'; 76 | } 77 | if (! $key) { 78 | $this->output->error('Config key missing'); 79 | } else { 80 | if (is_array($value)) { 81 | $this->output->error('Please specified the config key'); 82 | break; 83 | } 84 | $this->output->block([ 85 | sprintf('%s: %s', $key, $value), 86 | ]); 87 | } 88 | break; 89 | default: 90 | throw new \Exception('Unexpected action'); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/app/Command/CsFixProxyCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Debug.'); 32 | $this->ignoreValidationErrors(); 33 | } 34 | 35 | public function handle() 36 | { 37 | $this->output->writeln([ 38 | 'box version: v' . Box::VERSION, 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/Command/GetCommand.php: -------------------------------------------------------------------------------- 1 | downloadManager = $this->container->get(DownloadManager::class); 34 | } 35 | 36 | public function configure() 37 | { 38 | parent::configure(); 39 | $this->setDescription('Get the runtime or library into your project.'); 40 | $this->addArgument('pkg', InputArgument::REQUIRED, 'The package name'); 41 | $this->addOption('source', 's', InputOption::VALUE_OPTIONAL, 'The download source.'); 42 | $this->addOption('versions', '', InputOption::VALUE_NONE, 'Print the package versions only.'); 43 | $this->addOption('reinstall', 'r', InputOption::VALUE_NONE, 'Ignore the local file and reinstall.'); 44 | } 45 | 46 | public function handle() 47 | { 48 | Context::set(InputInterface::class, $this->input); 49 | Context::set(OutputInterface::class, $this->output); 50 | $pkg = $this->input->getArgument('pkg'); 51 | $source = $this->input->getOption('source'); 52 | $versions = $this->input->getOption('versions'); 53 | $reinstall = $this->input->getOption('reinstall'); 54 | [$pkg, $version] = $this->parsePkgVersion($pkg); 55 | $options = []; 56 | if ($source) { 57 | $options['source'] = $source; 58 | } 59 | if ($reinstall) { 60 | $options['reinstall'] = $reinstall; 61 | } 62 | if ($versions) { 63 | $versions = $this->downloadManager->versions($pkg, $options); 64 | if (empty($versions)) { 65 | $this->output->writeln('No versions found.'); 66 | } else { 67 | $this->output->writeln($versions); 68 | } 69 | } else { 70 | $this->downloadManager->get($pkg, $version, $options); 71 | } 72 | return self::SUCCESS; 73 | } 74 | 75 | protected function parsePkgVersion(string $pkg): array 76 | { 77 | $version = 'latest'; 78 | if (str_contains($pkg, '@')) { 79 | [$pkg, $version] = explode('@', $pkg); 80 | } 81 | return [$pkg, $version]; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/Command/HyperfCommand.php: -------------------------------------------------------------------------------- 1 | config = $this->container->get(Config::class); 29 | } 30 | 31 | public function configure() 32 | { 33 | parent::configure(); 34 | $this->setDescription('The hyperf proxy command.'); 35 | $this->ignoreValidationErrors(); 36 | } 37 | 38 | public function handle() 39 | { 40 | $path = $this->config->getConfig('path.runtime', getenv('HOME') . '/.box'); 41 | $kernel = strtolower($this->config->getConfig('kernel', 'swow')); 42 | $currentPhpVersion = $this->config->getConfig('versions.php', '8.1'); 43 | $hyperfBin = $this->config->getConfig('hyperf.bin', './bin/hyperf.php'); 44 | if ($kernel === 'swoole') { 45 | $closeShortname = "-d swoole.use_shortname='Off'"; 46 | $bin = $path . '/swoole-cli ' . $closeShortname . ' ' . $hyperfBin; 47 | if ($currentPhpVersion < '8.1') { 48 | $this->logger->warning(sprintf('Current setting PHP version is %s, but the kernel is Swoole and Swoole only support 8.1, so the PHP version is forced to 8.1.', $currentPhpVersion)); 49 | } 50 | } else { 51 | $bin = $path . '/php' . $currentPhpVersion . ' ' . $hyperfBin; 52 | } 53 | $command = Str::replaceFirst('hyperf ', '', (string) $this->input); 54 | $fullCommand = sprintf('%s %s', $bin, $command); 55 | $this->liveCommand($fullCommand); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/Command/PhpCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('The php proxy command.'); 32 | $this->ignoreValidationErrors(); 33 | } 34 | 35 | public function handle() 36 | { 37 | $bin = $this->buildBinPath(); 38 | $command = Str::replaceFirst('php ', '', (string) $this->input); 39 | $fullCommand = sprintf('%s %s', $bin, $command); 40 | $this->liveCommand($fullCommand); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/Command/PhpCsFixerProxyCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Start the reverse proxy server for upstreams.'); 41 | $this->addOption('host', '', InputOption::VALUE_OPTIONAL, 'The host of the reverse proxy server', '127.0.0.1'); 42 | $this->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'The port of the reverse proxy server', 9764); 43 | $this->addOption('backlog', '', InputOption::VALUE_OPTIONAL, 'The backlog of the reverse proxy server', Socket::DEFAULT_BACKLOG); 44 | $this->addOption('upstream', 'u', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The target upstream servers of the reverse proxy server', []); 45 | } 46 | 47 | public function handle() 48 | { 49 | $host = $this->input->getOption('host'); 50 | $port = (int) $this->input->getOption('port'); 51 | $backlog = (int) $this->input->getOption('backlog'); 52 | $upstreams = $this->input->getOption('upstream'); 53 | foreach ($upstreams as $key => $upstream) { 54 | [$upstreamHost, $upstreamPort] = explode(':', $upstream); 55 | $upstreams[$key] = [ 56 | 'host' => $upstreamHost, 57 | 'port' => (int) $upstreamPort, 58 | ]; 59 | } 60 | 61 | $server = new HttpServer(); 62 | $server->bind($host, $port)->listen($backlog); 63 | $this->output->writeln(sprintf('[INFO] Reverse Proxy Server listening at %s:%d', $host, $port)); 64 | while (true) { 65 | try { 66 | $connection = $server->acceptConnection(); 67 | Coroutine::run(static function () use ($connection, $upstreams): void { 68 | try { 69 | $target = $upstreams[array_rand($upstreams)]; 70 | $httpClient = new Client(); 71 | $httpClient->connect($target['host'], $target['port']); 72 | while (true) { 73 | $request = null; 74 | try { 75 | $request = $connection->recvHttpRequest(); 76 | $connection->sendHttpResponse( 77 | $httpClient->sendRequest($request->setKeepAlive(true)) 78 | ); 79 | } catch (ResponseException $exception) { 80 | $connection->error($exception->getCode(), $exception->getMessage()); 81 | } 82 | if (! $request || ! $request->getKeepAlive()) { 83 | break; 84 | } 85 | } 86 | } catch (Exception) { 87 | // you can log error here 88 | } finally { 89 | $connection->close(); 90 | isset($httpClient) && $httpClient->close(); 91 | } 92 | }); 93 | } catch (SocketException|CoroutineException $exception) { 94 | if (in_array($exception->getCode(), [Errno::EMFILE, Errno::ENFILE, Errno::ENOMEM], true)) { 95 | sleep(1); 96 | } else { 97 | break; 98 | } 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/app/Command/SelfUpdateCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Upgrade box to the latest version.'); 27 | } 28 | 29 | public function configure() 30 | { 31 | $this->addOption('reinstall', 'r', InputOption::VALUE_NONE, 'Ignore the local file and reinstall.'); 32 | } 33 | 34 | public function handle() 35 | { 36 | $this->call('get', ['pkg' => 'box@latest', '--reinstall' => $this->input->getOption('reinstall')]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/Command/VersionCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Dump the version of box.'); 32 | } 33 | 34 | public function handle() 35 | { 36 | $this->output->writeln([ 37 | 'box version: v' . Box::VERSION, 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/Config.php: -------------------------------------------------------------------------------- 1 | getBaseDir(); 26 | $this->configFilePath = $base . DIRECTORY_SEPARATOR . '.box'; 27 | $this->configFile = $this->configFilePath . DIRECTORY_SEPARATOR. '.boxconfig'; 28 | $this->init(); 29 | } 30 | 31 | public function getConfigContent(): array 32 | { 33 | return json_decode(file_get_contents($this->configFile), true); 34 | } 35 | 36 | public function setConfigContent(array $content): void 37 | { 38 | file_put_contents($this->configFile, json_encode($content, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); 39 | } 40 | 41 | public function updateConfig(string $key, ?string $value): void 42 | { 43 | $config = $this->getConfigContent(); 44 | if (in_array($value, ['bool', 'false'])) { 45 | $value = boolval($value); 46 | } 47 | Arr::set($config, $key, $value); 48 | $this->setConfigContent($config); 49 | } 50 | 51 | public function getConfig(?string $key, mixed $default = null): mixed 52 | { 53 | $config = $this->getConfigContent(); 54 | return Arr::get($config, $key, $default); 55 | } 56 | 57 | public function init(): void 58 | { 59 | if (! file_exists($this->configFile)) { 60 | if (! file_exists($this->configFilePath)) { 61 | mkdir($this->configFilePath, 0755, true); 62 | } 63 | file_put_contents($this->configFile, '{}'); 64 | chmod($this->configFile, 0755); 65 | } 66 | $content = $this->getConfigContent(); 67 | if (! $content) { 68 | $content = [ 69 | 'path' => [ 70 | 'runtime' => $this->getBaseDir(). DIRECTORY_SEPARATOR . '.box', 71 | 'bin' => $this->getBaseDir() . DIRECTORY_SEPARATOR . '.box', 72 | ], 73 | 'github' => [ 74 | 'access-token' => '', 75 | ], 76 | 'versions' => [ 77 | 'php' => '8.1', 78 | ], 79 | 'kernel' => 'swow', 80 | ]; 81 | $this->setConfigContent($content); 82 | } 83 | } 84 | 85 | protected function getBaseDir(): string 86 | { 87 | $base = getenv('HOME') ?: getenv('USERPROFILE'); 88 | return (string)$base; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/app/DownloadHandler/AbstractDownloadHandler.php: -------------------------------------------------------------------------------- 1 | runtimePath = $this->config->getConfig('path.runtime', getenv('HOME') . '/.box'); 51 | } 52 | 53 | abstract public function handle(string $pkgName, string $version, array $options = []): ?SplFileInfo; 54 | 55 | abstract public function versions(string $pkgName, array $options = []): array; 56 | 57 | protected function fetchDownloadUrlFromGithubRelease(string $assetName, string $fullRepo, string $version): ?string 58 | { 59 | $release = $this->githubClient->getRelease($fullRepo, $version); 60 | $url = null; 61 | foreach ($release['assets'] ?? [] as $asset) { 62 | if ($asset['name'] === $assetName && $asset['browser_download_url']) { 63 | $url = $asset['browser_download_url']; 64 | } 65 | } 66 | return $url; 67 | } 68 | 69 | protected function fetchVersionsFromGithubRelease(string $fullRepo, ?string $assetKeyword = null): array 70 | { 71 | $releases = $this->githubClient->getReleases($fullRepo); 72 | $versions = []; 73 | // Sort releases by published_at desc, also ignore error. 74 | usort($releases, function ($a, $b) { 75 | return strtotime($b['published_at'] ?? '0') <=> strtotime($a['published_at'] ?? '0'); 76 | }); 77 | // Filter releases which has assets, if the asset name is not null, then validate its. 78 | foreach ($releases as $release) { 79 | if (! empty($release['assets'])) { 80 | if ($assetKeyword) { 81 | foreach ($release['assets'] as $asset) { 82 | if (str_contains($asset['name'], $assetKeyword)) { 83 | $versions[] = $release['tag_name']; 84 | break; 85 | } 86 | } 87 | } else { 88 | $versions[] = $release['tag_name']; 89 | } 90 | } 91 | } 92 | return $versions; 93 | } 94 | 95 | protected function fetchVersionsFromPackagist(string $pkgName, string $composerName): array 96 | { 97 | $client = new Client([ 98 | 'verify' => false, 99 | ]); 100 | $response = $client->get(sprintf('https://repo.packagist.org/p2/%s.json', $composerName)); 101 | $body = json_decode($response->getBody()->getContents(), true); 102 | if (! isset($body['packages'][$composerName])) { 103 | throw new NotSupportVersionsException($pkgName); 104 | } 105 | $composerJsons = $body['packages'][$composerName]; 106 | $versions = []; 107 | foreach ($composerJsons as $composerJson) { 108 | if (isset($composerJson['version'])) { 109 | $versions[] = $composerJson['version']; 110 | } 111 | } 112 | // Sort versions by desc 113 | usort($versions, function ($a, $b) { 114 | return version_compare($b, $a); 115 | }); 116 | return $versions; 117 | } 118 | 119 | protected function download( 120 | ?string $url, 121 | string $savePath, 122 | int $permissions, 123 | string $renameTo = '' 124 | ): SplFileInfo { 125 | if (! $url) { 126 | throw new \RuntimeException('Download url is empty, maybe the url parse failed.'); 127 | } 128 | $this->logger->info(sprintf('Downloading %s', $url)); 129 | if (str_ends_with($savePath, DIRECTORY_SEPARATOR)) { 130 | $explodedUrl = explode('/', $url); 131 | $filename = end($explodedUrl); 132 | $savePath = $savePath . $filename; 133 | } 134 | $sink = $savePath . '.tmp'; 135 | 136 | $channel = new Channel(1); 137 | $client = new Client([ 138 | 'sink' => $sink, 139 | 'progress' => static function ( 140 | $downloadTotal, 141 | $downloadedBytes, 142 | ) use ($channel) { 143 | $channel->push([$downloadedBytes, $downloadTotal]); 144 | }, 145 | 'verify' => false, 146 | ]); 147 | 148 | /** @var OutputInterface $output */ 149 | $output = Context::get(OutputInterface::class); 150 | $this->showProgressBar($output, $channel); 151 | 152 | $client->get($url); 153 | 154 | $output->writeln(''); 155 | $output->writeln(''); 156 | if ($renameTo) { 157 | $explodedSavePath = explode(DIRECTORY_SEPARATOR, $savePath); 158 | $filename = end($explodedSavePath); 159 | rename($sink, $afterRenameSavePath = Str::replaceLast($filename, $renameTo, $savePath)); 160 | $savePath = $afterRenameSavePath; 161 | } else { 162 | rename($sink, $savePath); 163 | } 164 | chmod($savePath, $permissions); 165 | $this->logger->info(sprintf('Download saved to %s', $savePath)); 166 | return new SplFileInfo($savePath); 167 | } 168 | 169 | protected function showProgressBar(OutputInterface $output, Channel $channel): void 170 | { 171 | Coroutine::create(function () use ($output, $channel) { 172 | $progressBar = new ProgressBar($output); 173 | $progressBar->setFormat('%current% kb [%bar%]'); 174 | while ([$downloadedBytes, $downloadTotal] = $channel->pop(-1)) { 175 | if ($downloadTotal && $progressBar->getMaxSteps() !== $downloadTotal) { 176 | $progressBar->setMaxSteps($this->byteToKb($downloadTotal)); 177 | $progressBar->setFormat('%current% kb / %max% kb [%bar%] %percent:3s%% %elapsed:6s% / %estimated:-6s%'); 178 | } 179 | $downloadedBytes && $progressBar->setProgress($this->byteToKb($downloadedBytes)); 180 | if ($downloadTotal && $downloadedBytes >= $downloadTotal) { 181 | break; 182 | } 183 | } 184 | $progressBar->display(); 185 | $progressBar->finish(); 186 | $channel->close(); 187 | }); 188 | } 189 | 190 | protected function byteToKb(int $byte): int 191 | { 192 | return (int)ceil($byte / 1024); 193 | } 194 | 195 | protected function getDefinition(string $pkgName): ?Definition 196 | { 197 | return $this->pkgsDefinitionManager->getDefinition($pkgName); 198 | } 199 | 200 | protected function replaces(string $subject, array $replaces): string 201 | { 202 | foreach ($replaces as $search => $replace) { 203 | $subject = str_replace('${{' . $search . '}}', $replace, $subject); 204 | } 205 | return $subject; 206 | } 207 | 208 | protected function latestVersionCheck(string $currentVersion, Definition $definition, array $options = []): void 209 | { 210 | if (isset($options['reinstall']) && $options['reinstall']) { 211 | return; 212 | } 213 | $latestVersion = $this->versions($definition->getPkgName())[0] ?? ''; 214 | // Trim v prefix 215 | $latestVersion = ltrim($latestVersion, 'v'); 216 | if ($latestVersion && version_compare($currentVersion, $latestVersion, '>=')) { 217 | throw new BoxException(sprintf('Your %s version %s is latest, no need to update, add `--reinstall` or `-r` option to force reinstall the package.', $definition->getPkgName(), $currentVersion)); 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/app/DownloadHandler/BoxHandler.php: -------------------------------------------------------------------------------- 1 | getDefinition($pkgName); 31 | $this->latestVersionCheck(Box::VERSION, $definition, $options); 32 | $assetName = $definition->getReleaseAssetMatchRule()[PHP_OS_FAMILY . '.' . php_uname('m')] ?? ''; 33 | if (! $assetName) { 34 | throw new BoxException('Can not found any matched asset for your system.'); 35 | } 36 | $url = $this->fetchDownloadUrlFromGithubRelease($assetName, $definition->getRepo(), $version); 37 | $savePath = Phar::running(false) ?: $this->runtimePath . DIRECTORY_SEPARATOR; 38 | 39 | return $this->download($url, $savePath, 0755, $definition->getBin()); 40 | } 41 | 42 | public function versions(string $pkgName, array $options = []): array 43 | { 44 | $definition = $this->getDefinition($pkgName); 45 | return $this->fetchVersionsFromGithubRelease($definition->getRepo(), $definition->getReleaseAssetKeyword()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/DownloadHandler/ComposerHandler.php: -------------------------------------------------------------------------------- 1 | getDefinition($pkgName); 27 | if (! isset($options['source'])) { 28 | $options['source'] = $definition->getSources()->getSource('default')?->getUrl(); 29 | } 30 | $url = match ($options['source']) { 31 | $this->githubBaseUrl => $this->fetchDownloadUrlFromGithubRelease($definition->getBin(), $definition->getRepo(), $version), 32 | default => $this->fetchDownloadUrlFromGetComposerOrg($definition, $version), 33 | }; 34 | return $this->download($url, $this->runtimePath . DIRECTORY_SEPARATOR , 0755); 35 | } 36 | 37 | public function versions(string $pkgName, array $options = []): array 38 | { 39 | $definition = $this->getDefinition($pkgName); 40 | if (! isset($options['source'])) { 41 | $options['source'] = $definition->getSources()->getSource('default')?->getUrl(); 42 | } 43 | return match ($options['source']) { 44 | $this->githubBaseUrl => $this->fetchVersionsFromGithubRelease($definition->getRepo()), 45 | default => throw new NotSupportVersionsException($pkgName), 46 | }; 47 | } 48 | 49 | protected function fetchDownloadUrlFromGetComposerOrg(Definition $definition, string $version): string 50 | { 51 | if ($version === 'latest') { 52 | $release = $this->githubClient->getRelease($definition->getRepo(), $version); 53 | if (! isset($release['tag_name'])) { 54 | throw new \RuntimeException('Cannot match the specified version from github releases.'); 55 | } 56 | $specifiedVersion = $release['tag_name']; 57 | } else { 58 | $specifiedVersion = $version; 59 | } 60 | $url = $definition->getSources()?->getSource('getcomposer.org')?->getUrl(); 61 | if (! $url) { 62 | throw new \RuntimeException('Cannot parse the download url by getcomposer.org.'); 63 | } 64 | return $this->replaces($url, [ 65 | 'version' => $specifiedVersion, 66 | 'bin' => $definition->getBin(), 67 | ]); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/DownloadHandler/DefaultHandler.php: -------------------------------------------------------------------------------- 1 | getDefinition($pkgName); 24 | if (! $definition) { 25 | throw new \RuntimeException('The package not found'); 26 | } 27 | if ($definition->getRepo()) { 28 | $url = $this->fetchDownloadUrlFromGithubRelease($definition->getBin(), $definition->getRepo(), $version); 29 | } elseif ($definition->getUrl()) { 30 | if ($version === 'latest') { 31 | if ($definition->getLatest() && $definition->getLatest() !== 'latest') { 32 | $specifiedVersion = $definition->getLatest(); 33 | } else { 34 | $versions = $this->versions($pkgName); 35 | $specifiedVersion = array_shift($versions); 36 | } 37 | } else { 38 | $specifiedVersion = $version; 39 | } 40 | $url = str_replace('${{version}}', $specifiedVersion, $definition->getUrl()); 41 | } else { 42 | throw new \RuntimeException('The definition of package is invalid'); 43 | } 44 | return $this->download($url, $this->runtimePath . DIRECTORY_SEPARATOR, 0755, $definition->getBin()); 45 | } 46 | 47 | public function versions(string $pkgName, array $options = []): array 48 | { 49 | $definition = $this->getDefinition($pkgName); 50 | if (! $definition) { 51 | throw new \RuntimeException('The package not found'); 52 | } 53 | if (! $definition->getRepo() && ! $definition->getComposerName()) { 54 | throw new NotSupportVersionsException($pkgName); 55 | } 56 | if ($definition->getLatestFetchType() === 'github') { 57 | return $this->fetchVersionsFromGithubRelease($definition->getRepo(), $definition->getBin()); 58 | } elseif ($definition->getLatestFetchType() === 'packagist') { 59 | return $this->fetchVersionsFromPackagist($definition->getPkgName(), $definition->getComposerName()); 60 | } else { 61 | throw new BoxException('The definition of package is invalid'); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/DownloadHandler/MicroHandler.php: -------------------------------------------------------------------------------- 1 | prehandleVersion($version); 29 | try { 30 | $response = $this->getArtifact($this->getDefinition($pkgName), $version, 'micro'); 31 | if ($response->getStatusCode() !== 302 || ! $response->getHeaderLine('Location')) { 32 | throw new \RuntimeException('Download failed, cannot retrieve the download url from artifact.'); 33 | } 34 | $savePath = $this->runtimePath . DIRECTORY_SEPARATOR . 'micro_php' . $version . '.zip'; 35 | $this->download($response->getHeaderLine('Location'), $savePath, 0755); 36 | if (! file_exists($savePath)) { 37 | throw new \RuntimeException('Download failed, cannot locate the PHP bin file in local.'); 38 | } 39 | // Unzip the artifact file 40 | $this->logger->info('Unpacking zip file ' . $savePath); 41 | $renameTo = $this->runtimePath . DIRECTORY_SEPARATOR . 'micro_php' . $version . '.sfx'; 42 | $zip = new ZipArchive(); 43 | $zip->open($savePath); 44 | for ($i = 0; $i < $zip->numFiles; ++$i) { 45 | $filename = $zip->getNameIndex($i); 46 | if ($filename === 'micro.sfx') { 47 | copy('zip://' . $savePath . '#' . $filename, $renameTo); 48 | } 49 | } 50 | $zip->close(); 51 | $this->logger->info('Unpacked zip file ' . $savePath); 52 | unlink($savePath); 53 | $this->logger->info(sprintf('Deleted %s', $savePath)); 54 | return new SplFileInfo($renameTo); 55 | } catch (GuzzleException $exception) { 56 | $this->logger->error($exception->getMessage()); 57 | } 58 | return null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/app/DownloadHandler/PhpHandler.php: -------------------------------------------------------------------------------- 1 | prehandleVersion($version); 32 | try { 33 | $response = $this->getArtifact($this->getDefinition($pkgName), $version, 'cli'); 34 | if ($response->getStatusCode() !== 302 || ! $response->getHeaderLine('Location')) { 35 | throw new \RuntimeException('Download failed, cannot retrieve the download url from artifact.'); 36 | } 37 | $savePath = $this->runtimePath . DIRECTORY_SEPARATOR . 'php' . $version . '.zip'; 38 | $this->download($response->getHeaderLine('Location'), $savePath, 0755); 39 | if (! file_exists($savePath)) { 40 | throw new \RuntimeException('Download failed, cannot locate the PHP bin file in local.'); 41 | } 42 | // Unzip the artifact file 43 | $this->logger->info('Unpacking zip file ' . $savePath); 44 | $extention = ''; 45 | if (PHP_OS_FAMILY === 'Windows') { 46 | $extention = '.exe'; 47 | } 48 | $renameTo = $this->runtimePath . DIRECTORY_SEPARATOR . 'php' . $version . $extention; 49 | $zip = new ZipArchive(); 50 | $zip->open($savePath); 51 | for ($i = 0; $i < $zip->numFiles; ++$i) { 52 | $filename = $zip->getNameIndex($i); 53 | if ($filename === 'php' || $filename === 'php.exe') { 54 | copy('zip://' . $savePath . '#' . $filename, $renameTo); 55 | } 56 | } 57 | $zip->close(); 58 | $this->logger->info('Unpacked zip file ' . $savePath); 59 | unlink($savePath); 60 | $this->logger->info(sprintf('Deleted %s', $savePath)); 61 | return new SplFileInfo($renameTo); 62 | } catch (GuzzleException $exception) { 63 | $this->logger->error($exception->getMessage()); 64 | } 65 | return null; 66 | } 67 | 68 | protected function matchArtifact(array $artifacts, string $search): array 69 | { 70 | foreach ($artifacts as $artifact) { 71 | if (str_contains($artifact['name'], $search)) { 72 | return $artifact; 73 | } 74 | } 75 | return []; 76 | } 77 | 78 | protected function prehandleVersion(string $version): string 79 | { 80 | if ($version === 'latest') { 81 | $version = '8.1'; 82 | } 83 | return $version; 84 | } 85 | 86 | /** 87 | * @throws \GuzzleHttp\Exception\GuzzleException 88 | */ 89 | protected function getArtifact(Definition $definition, string $version, string $prefix): ResponseInterface 90 | { 91 | $githubToken = $this->githubClient->getGithubToken(); 92 | if (! $githubToken) { 93 | throw new \RuntimeException('Missing github access token, run `box config set github.access-token ` to complete the configuration.'); 94 | } 95 | $os = PHP_OS_FAMILY; 96 | $arch = php_uname('m'); 97 | $key = $os . '.' . $arch; 98 | $response = $this->githubClient->getActionsArtifacts($definition->getRepo(), $definition->getJobs()?->getJob($key)?->getJobId()); 99 | $searchKey = $this->buildSearchKey($definition, $key, $prefix, $version, $arch); 100 | $artifact = $this->matchArtifact($response['artifacts'] ?? [], $searchKey); 101 | if (! isset($artifact['archive_download_url'])) { 102 | throw new \RuntimeException('Does not match any artifact.'); 103 | } 104 | return $this->httpClient->get($artifact['archive_download_url'], [ 105 | RequestOptions::HEADERS => [ 106 | 'Accept' => 'application/vnd.github.v3+json', 107 | 'Authorization' => 'token ' . $githubToken, 108 | ], 109 | RequestOptions::ALLOW_REDIRECTS => false, 110 | RequestOptions::VERIFY => false, 111 | ]); 112 | } 113 | 114 | protected function buildSearchKey(Definition $definition, string $key, string $prefix, string $version, string $arch): string 115 | { 116 | return $this->replaces($definition->getJobArtifactMatchRule()[$key], [ 117 | 'prefix' => $prefix, 118 | 'php-version' => $version, 119 | 'arch' => $arch, 120 | ]); 121 | } 122 | 123 | protected function isBinExists(string $string): bool 124 | { 125 | $result = shell_exec(sprintf("which %s", escapeshellarg($string))); 126 | return ! empty($result) && ! str_contains($result, 'not found'); 127 | } 128 | 129 | public function versions(string $pkgName, array $options = []): array 130 | { 131 | return $this->getDefinition($pkgName)->getVersions(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/app/DownloadHandler/PintHandler.php: -------------------------------------------------------------------------------- 1 | getDefinition($pkgName); 24 | if (! $definition) { 25 | throw new \RuntimeException('The package not found'); 26 | } 27 | if ($definition->getRepo()) { 28 | $url = $this->fetchDownloadUrlFromGithubRelease($definition->getBin(), $definition->getRepo(), $version); 29 | } elseif ($definition->getUrl()) { 30 | if ($version === 'latest') { 31 | if ($definition->getLatest() && $definition->getLatest() !== 'latest') { 32 | $specifiedVersion = $definition->getLatest(); 33 | } else { 34 | $versions = $this->versions($pkgName); 35 | $specifiedVersion = array_shift($versions); 36 | } 37 | } else { 38 | $specifiedVersion = $version; 39 | } 40 | $url = str_replace('${{version}}', $specifiedVersion, $definition->getUrl()); 41 | } else { 42 | throw new \RuntimeException('The definition of package is invalid'); 43 | } 44 | 45 | $savePath = $this->runtimePath . DIRECTORY_SEPARATOR; 46 | $file = $this->download($url, $savePath, 0755); 47 | if (! file_exists($savePath)) { 48 | throw new \RuntimeException('Download failed, cannot locate the file in local.'); 49 | } 50 | 51 | // Is file name ends with .zip? 52 | if (str_ends_with($file->getFilename(), '.zip')) { 53 | $zip = new \ZipArchive(); 54 | $zip->open($file->getRealPath()); 55 | $this->logger->info('Unpacking zip file ' . $savePath); 56 | for ($i = 0; $i < $zip->numFiles; ++$i) { 57 | $filename = $zip->getNameIndex($i); 58 | if (str_ends_with($filename, 'builds/pint')) { 59 | copy('zip://' . $file->getRealPath() . '#' . $filename, $savePath . $definition->getBin()); 60 | } 61 | } 62 | $zip->close(); 63 | $this->logger->info('Unpacked zip file ' . $savePath); 64 | unlink($file->getRealPath()); 65 | } 66 | 67 | $licenseFile = $savePath . 'LICENSE'; 68 | // If license file exists, delete it. 69 | 70 | if (file_exists($licenseFile)) { 71 | unlink($licenseFile); 72 | } 73 | 74 | return new SplFileInfo($savePath . $definition->getBin()); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/app/DownloadHandler/SwooleCliHandler.php: -------------------------------------------------------------------------------- 1 | getDefinition($pkgName); 25 | if ($version === 'latest') { 26 | if ($definition->getLatest() !== null && $definition->getLatest() !== 'latest') { 27 | $specifiedVersion = $definition->getLatest(); 28 | } else { 29 | $release = $this->githubClient->getRelease($definition->getRepo(), $version); 30 | if (! isset($release['tag_name'])) { 31 | throw new \RuntimeException('Cannot match the specified version from github releases.'); 32 | } 33 | $specifiedVersion = $release['tag_name']; 34 | } 35 | } else { 36 | $specifiedVersion = $version; 37 | } 38 | $matchRules = $definition->getReleaseAssetMatchRule(); 39 | $matchRule = null; 40 | $os = PHP_OS; 41 | if (isset($matchRules[$os])) { 42 | $matchRule = $matchRules[$os]; 43 | } 44 | if (! $matchRule) { 45 | throw new PackageNotFoundException($pkgName); 46 | } else { 47 | $assetName = $this->replaces($matchRule, ['version' => $specifiedVersion]); 48 | } 49 | $url = $this->fetchDownloadUrlFromGithubRelease($assetName, $definition->getRepo(), $specifiedVersion); 50 | $savePath = $this->runtimePath . DIRECTORY_SEPARATOR; 51 | 52 | $file = $this->download($url, $savePath, 0755); 53 | if (! file_exists($savePath)) { 54 | throw new \RuntimeException('Download failed, cannot locate the file in local.'); 55 | } 56 | // Is file name ends with .zip? 57 | if (str_ends_with($file->getFilename(), '.zip')) { 58 | $zip = new \ZipArchive(); 59 | $zip->open($file->getRealPath()); 60 | $this->logger->info('Unpacking zip file ' . $savePath); 61 | $zip->extractTo($savePath); 62 | $zip->close(); 63 | $this->logger->info('Unpacked zip file ' . $savePath); 64 | unlink($file->getRealPath()); 65 | } 66 | // Is file name ends with .tar.xz? 67 | if (str_ends_with($file->getFilename(), '.tar.xz')) { 68 | $this->logger->info('Unpacking tar.xz file ' . $savePath); 69 | exec(sprintf('tar -xvf %s -C %s', $file->getRealPath(), $savePath)); 70 | $this->logger->info('Unpacked tar.xz file ' . $savePath); 71 | unlink($file->getRealPath()); 72 | } 73 | // Is file name ends with .tar.gz? 74 | if (str_ends_with($file->getFilename(), '.tar.gz')) { 75 | $this->logger->info('Unpacking tar.gz file ' . $savePath); 76 | exec(sprintf('tar -xvf %s -C %s', $file->getRealPath(), $savePath)); 77 | $this->logger->info('Unpacked tar.gz file ' . $savePath); 78 | unlink($file->getRealPath()); 79 | } 80 | $licenseFile = $savePath . 'LICENSE'; 81 | // If license file exists, delete it. 82 | if (file_exists($licenseFile)) { 83 | unlink($licenseFile); 84 | } 85 | return new SplFileInfo($savePath . $definition->getBin()); 86 | } 87 | 88 | public function versions(string $pkgName, array $options = []): array 89 | { 90 | $definition = $this->getDefinition($pkgName); 91 | return $this->fetchVersionsFromGithubRelease($definition->getRepo(), $definition->getReleaseAssetKeyword()); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/app/DownloadManager.php: -------------------------------------------------------------------------------- 1 | BoxHandler::class, 33 | 'composer' => ComposerHandler::class, 34 | 'micro' => MicroHandler::class, 35 | 'php' => PhpHandler::class, 36 | 'pint' => PintHandler::class, 37 | 'swoole-cli' => SwooleCliHandler::class, 38 | 'default' => DefaultHandler::class, 39 | ]; 40 | 41 | #[Inject] 42 | protected ContainerInterface $container; 43 | 44 | #[Inject] 45 | protected Config $config; 46 | 47 | #[Inject] 48 | protected PkgDefinitionManager $pkgDefinitionManager; 49 | 50 | public function get(string $pkg, string $version, array $options = []): void 51 | { 52 | $handler = $this->buildHandler($pkg); 53 | $this->createRuntimePath(); 54 | $file = $handler->handle($pkg, $version, $options); 55 | if ($file instanceof SplFileInfo && $file->isWritable()) { 56 | chmod($file->getRealPath(), 0755); 57 | } 58 | } 59 | 60 | public function versions(string $pkg, array $options): array 61 | { 62 | $handler = $this->buildHandler($pkg); 63 | return $handler->versions($pkg, $options); 64 | } 65 | 66 | protected function createRuntimePath(): void 67 | { 68 | $base = getenv('HOME') ?: getenv('USERPROFILE'); 69 | $path = $this->config->getConfig('path.runtime', $base . DIRECTORY_SEPARATOR . '.box'); 70 | if (! file_exists($path)) { 71 | mkdir($path, 0755); 72 | chmod($path, 0755); 73 | } 74 | } 75 | 76 | protected function buildHandler(string &$pkg): AbstractDownloadHandler 77 | { 78 | if (! $this->pkgDefinitionManager->hasDefinition($pkg)) { 79 | throw new PkgDefinitionNotFoundException($pkg); 80 | } 81 | $key = 'default'; 82 | if (isset($this->handlers[$pkg])) { 83 | $key = $pkg; 84 | } 85 | $kernel = strtolower($this->config->getConfig('kernel', 'swow')); 86 | if ($key === 'php' && $kernel === 'swoole') { 87 | $key = $pkg = 'swoole-cli'; 88 | } 89 | return $this->container->get($this->handlers[$key]); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/app/Exception/BoxException.php: -------------------------------------------------------------------------------- 1 | httpClient = new Client([ 31 | 'headers' => [ 32 | 'Accept' => 'application/vnd.github.v3+json', 33 | 'Authorization' => 'token ' . $this->getGithubToken(), 34 | ], 35 | 'verify' => false 36 | ]); 37 | } 38 | 39 | public function getRelease(string $repo, string $version): array 40 | { 41 | $baseUrl = $this->buildRepoUrl($repo); 42 | if ($version == 'latest') { 43 | $url = $baseUrl . '/releases/' . $version; 44 | } else { 45 | $url = $baseUrl . '/releases/tags/' . $version; 46 | } 47 | $response = $this->httpClient->get($url); 48 | return json_decode($response->getBody()->getContents(), true); 49 | } 50 | 51 | public function getReleases(string $fullRepo): array 52 | { 53 | $url = $this->buildRepoUrl($fullRepo) . '/releases'; 54 | $response = $this->httpClient->get($url); 55 | return json_decode($response->getBody()->getContents(), true); 56 | } 57 | 58 | public function getActionsArtifacts(string $repo, string $jobId, ?int $page = null) 59 | { 60 | $url = $this->buildRepoUrl($repo) . '/actions/runs/' . $jobId . '/artifacts'; 61 | if ($page) { 62 | $url .= '?page=' . $page; 63 | } 64 | $response = $this->httpClient->get($url); 65 | $artifacts = json_decode($response->getBody()->getContents(), true); 66 | if ($response->hasHeader('link')) { 67 | $links = []; 68 | $link = $response->getHeaderLine('link'); 69 | $explodedLinks = explode(', ', $link); 70 | foreach ($explodedLinks as $explodedLink) { 71 | [$link, $rel] = explode(';', $explodedLink); 72 | $link = trim($link, '<>'); 73 | $pageNum = Str::afterLast($link, '='); 74 | $rel = str_replace('rel=', '', trim($rel)); 75 | $rel = str_replace('"', '', $rel); 76 | $links[$rel] = ['link' => $link, 'page' => $pageNum]; 77 | } 78 | if (isset($links['next']['page']) && is_numeric($links['next']['page'])) { 79 | $nextPageArtifacts = $this->getActionsArtifacts($repo, $jobId, intval($links['next']['page'])); 80 | $artifacts['artifacts'] = array_merge($artifacts['artifacts'], $nextPageArtifacts['artifacts']); 81 | } 82 | } 83 | return $artifacts; 84 | } 85 | 86 | public function getGithubToken(): string 87 | { 88 | return $this->config->getConfig('github.access-token') ?: ''; 89 | } 90 | 91 | protected function buildRepoUrl(string $repo): string 92 | { 93 | return $this->baseUrl . 'repos/' . $repo; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/app/PkgDefinition/Definition.php: -------------------------------------------------------------------------------- 1 | setPkgName($pkgName); 24 | isset($data['repo']) && is_string($data['repo']) && $this->setRepo($data['repo']); 25 | isset($data['bin']) && is_string($data['bin']) && $this->setBin($data['bin']); 26 | isset($data['latest']) && is_string($data['latest']) && $this->setLatest($data['latest']); 27 | isset($data['latest_fetch_type']) && is_string($data['latest_fetch_type']) && $this->setLatestFetchType($data['latest_fetch_type']); 28 | isset($data['url']) && is_string($data['url']) && $this->setUrl($data['url']); 29 | isset($data['composer_name']) && is_string($data['composer_name']) && $this->setComposerName($data['composer_name']); 30 | isset($data['jobs']) && is_array($data['jobs']) && $this->setJobs(new Jobs($data['jobs'])); 31 | isset($data['job_artifact_match_rule']) && is_array($data['job_artifact_match_rule']) && $this->setJobArtifactMatchRule($data['job_artifact_match_rule']); 32 | isset($data['release_asset_match_rule']) && is_array($data['release_asset_match_rule']) && $this->setReleaseAssetMatchRule($data['release_asset_match_rule']); 33 | isset($data['release_asset_keyword']) && is_string($data['release_asset_keyword']) && $this->setReleaseAssetKeyword($data['release_asset_keyword']); 34 | isset($data['versions']) && is_array($data['versions']) && $this->setVersions($data['versions']); 35 | isset($data['sources']) && is_array($data['sources']) && $this->setSources(new Sources($data['sources'])); 36 | } 37 | 38 | public function getRepo(): ?string 39 | { 40 | return $this->repo; 41 | } 42 | 43 | public function setRepo(?string $repo): Definition 44 | { 45 | $this->repo = $repo; 46 | return $this; 47 | } 48 | 49 | public function getBin(): ?string 50 | { 51 | return $this->bin; 52 | } 53 | 54 | public function setBin(?string $bin): Definition 55 | { 56 | $this->bin = $bin; 57 | return $this; 58 | } 59 | 60 | public function getLatest(): ?string 61 | { 62 | return $this->latest; 63 | } 64 | 65 | public function setLatest(?string $latest): Definition 66 | { 67 | $this->latest = $latest; 68 | return $this; 69 | } 70 | 71 | public function getLatestFetchType(): ?string 72 | { 73 | return $this->latestFetchType; 74 | } 75 | 76 | public function setLatestFetchType(?string $latestFetchType): Definition 77 | { 78 | $this->latestFetchType = $latestFetchType; 79 | return $this; 80 | } 81 | 82 | public function getUrl(): ?string 83 | { 84 | return $this->url; 85 | } 86 | 87 | public function setUrl(?string $url): Definition 88 | { 89 | $this->url = $url; 90 | return $this; 91 | } 92 | 93 | public function getJobs(): ?Jobs 94 | { 95 | return $this->jobs; 96 | } 97 | 98 | public function setJobs(?Jobs $jobs): Definition 99 | { 100 | $this->jobs = $jobs; 101 | return $this; 102 | } 103 | 104 | public function getJobArtifactMatchRule(): array 105 | { 106 | return $this->jobArtifactMatchRule; 107 | } 108 | 109 | public function setJobArtifactMatchRule(array $jobArtifactMatchRule): Definition 110 | { 111 | $this->jobArtifactMatchRule = $jobArtifactMatchRule; 112 | return $this; 113 | } 114 | 115 | public function getReleaseAssetMatchRule(): array 116 | { 117 | return $this->releaseAssetMatchRule; 118 | } 119 | 120 | public function setReleaseAssetMatchRule(array $releaseAssetMatchRule): Definition 121 | { 122 | $this->releaseAssetMatchRule = $releaseAssetMatchRule; 123 | return $this; 124 | } 125 | 126 | public function getVersions(): array 127 | { 128 | return $this->versions; 129 | } 130 | 131 | public function setVersions(array $versions): Definition 132 | { 133 | $this->versions = $versions; 134 | return $this; 135 | } 136 | 137 | public function getSources(): Sources 138 | { 139 | return $this->sources; 140 | } 141 | 142 | public function setSources(Sources $sources): Definition 143 | { 144 | $this->sources = $sources; 145 | return $this; 146 | } 147 | 148 | public function getReleaseAssetKeyword(): string 149 | { 150 | return $this->releaseAssetKeyword; 151 | } 152 | 153 | public function setReleaseAssetKeyword(string $releaseAssetKeyword): Definition 154 | { 155 | $this->releaseAssetKeyword = $releaseAssetKeyword; 156 | return $this; 157 | } 158 | 159 | public function getComposerName(): ?string 160 | { 161 | return $this->composerName; 162 | } 163 | 164 | public function setComposerName(string $composerName): Definition 165 | { 166 | $this->composerName = $composerName; 167 | return $this; 168 | } 169 | 170 | public function getPkgName(): string 171 | { 172 | return $this->pkgName; 173 | } 174 | 175 | public function setPkgName(string $pkgName): Definition 176 | { 177 | $this->pkgName = $pkgName; 178 | return $this; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/app/PkgDefinition/Job.php: -------------------------------------------------------------------------------- 1 | os = explode('.', $osAndArch)[0] ?? 'Unknown'; 14 | $this->arch = explode('.', $osAndArch)[1] ?? 'x86_64'; 15 | $this->jobId = $jobId; 16 | } 17 | 18 | public function getOs(): string 19 | { 20 | return $this->os; 21 | } 22 | 23 | public function setOs(string $os): Job 24 | { 25 | $this->os = $os; 26 | return $this; 27 | } 28 | 29 | public function getArch(): string 30 | { 31 | return $this->arch; 32 | } 33 | 34 | public function setArch(string $arch): Job 35 | { 36 | $this->arch = $arch; 37 | return $this; 38 | } 39 | 40 | public function getJobId(): string 41 | { 42 | return $this->jobId; 43 | } 44 | 45 | public function setJobId(string $jobId): Job 46 | { 47 | $this->jobId = $jobId; 48 | return $this; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/PkgDefinition/Jobs.php: -------------------------------------------------------------------------------- 1 | $jobId) { 15 | $this->jobs[$arch] = new Job($arch, $jobId); 16 | } 17 | } 18 | 19 | public function getJob(string $arch): ?Job 20 | { 21 | $job = $this->jobs[$arch] ?? null; 22 | return $job instanceof Job ? $job : null; 23 | } 24 | 25 | public function getJobs(): array 26 | { 27 | return $this->jobs; 28 | } 29 | 30 | public function setJobs(array $jobs): static 31 | { 32 | $this->jobs = $jobs; 33 | return $this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/PkgDefinition/Source.php: -------------------------------------------------------------------------------- 1 | setName($name); 14 | isset($data['type']) && is_string($data['type']) && $this->setType($data['type']); 15 | isset($data['url']) && is_string($data['url']) && $this->setUrl($data['url']); 16 | } 17 | 18 | public function getName(): string 19 | { 20 | return $this->name; 21 | } 22 | 23 | public function setName(string $name): Source 24 | { 25 | $this->name = $name; 26 | return $this; 27 | } 28 | 29 | public function getType(): string 30 | { 31 | return $this->type; 32 | } 33 | 34 | public function setType(string $type): Source 35 | { 36 | $this->type = $type; 37 | return $this; 38 | } 39 | 40 | public function getUrl(): string 41 | { 42 | return $this->url; 43 | } 44 | 45 | public function setUrl(string $url): Source 46 | { 47 | $this->url = $url; 48 | return $this; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/PkgDefinition/Sources.php: -------------------------------------------------------------------------------- 1 | $data) { 12 | $this->sources[$name] = new Source($name, $data); 13 | } 14 | } 15 | 16 | public function getSource(string $name): ?Source 17 | { 18 | $source = $this->sources[$name] ?? null; 19 | return $source instanceof Source ? $source : null; 20 | } 21 | 22 | public function getSources(): array 23 | { 24 | return $this->sources; 25 | } 26 | 27 | public function setSources(array $sources): Sources 28 | { 29 | // Validate the value is an array of Source objects 30 | foreach ($sources as $source) { 31 | if (!($source instanceof Source)) { 32 | throw new \InvalidArgumentException('Invalid value for sources'); 33 | } 34 | } 35 | $this->sources = $sources; 36 | return $this; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/PkgDefinitionManager.php: -------------------------------------------------------------------------------- 1 | getPkgs() as $name => $item) { 26 | if ($name === $pkg) { 27 | return new Definition($name, $item); 28 | } 29 | } 30 | return null; 31 | } 32 | 33 | public function hasDefinition(string $pkg): bool 34 | { 35 | return isset($this->getPkgs()[$pkg]); 36 | } 37 | 38 | public function getPkgs(): array 39 | { 40 | if (is_null($this->pkgs)) { 41 | $this->fetchPkgs(); 42 | } 43 | return $this->pkgs; 44 | } 45 | 46 | public function fetchPkgs(): bool 47 | { 48 | try { 49 | // Is url start with file:// ? 50 | if (str_starts_with($this->url, 'file://')) { 51 | $path = substr($this->url, 7); 52 | if (! file_exists($path)) { 53 | $this->logger->error(sprintf('File %s not exists.', $path)); 54 | return false; 55 | } 56 | $this->pkgs = json_decode(file_get_contents($path), true); 57 | } else { 58 | $response = (new Client(['verify' => false]))->get($this->url); 59 | if ($response->getStatusCode() === 200) { 60 | $this->pkgs = json_decode($response->getBody()->getContents(), true); 61 | } 62 | } 63 | } catch (\Throwable $exception) { 64 | $this->logger->error($exception->getMessage()); 65 | return false; 66 | } 67 | return true; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/bin/hyperf.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | get(Hyperf\Contract\ApplicationInterface::class); 33 | $application->run(); 34 | })(); 35 | -------------------------------------------------------------------------------- /src/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf/box", 3 | "type": "project", 4 | "keywords": [ 5 | "php", 6 | "swow", 7 | "framework", 8 | "hyperf", 9 | "microservice", 10 | "middleware" 11 | ], 12 | "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.", 13 | "license": "MIT", 14 | "require": { 15 | "php": ">=8.1", 16 | "ext-json": "*", 17 | "ext-openssl": "*", 18 | "hyperf/command": "3.0.*", 19 | "hyperf/config": "3.0.*", 20 | "hyperf/constants": "3.0.*", 21 | "hyperf/contract": "3.0.*", 22 | "hyperf/di": "3.0.*", 23 | "hyperf/dispatcher": "3.0.*", 24 | "hyperf/engine-swow": "^2.1", 25 | "hyperf/event": "3.0.*", 26 | "hyperf/exception-handler": "3.0.*", 27 | "hyperf/framework": "3.0.*", 28 | "hyperf/guzzle": "3.0.*", 29 | "hyperf/http-server": "3.0.*", 30 | "hyperf/logger": "3.0.*", 31 | "hyperf/pool": "3.0.*", 32 | "hyperf/phar": "3.0.*", 33 | "hyperf/process": "3.0.*", 34 | "hyperf/utils": "3.0.*", 35 | "hyperf/server": "3.0.*", 36 | "guzzlehttp/guzzle": "^7.0", 37 | "guzzlehttp/psr7": "^2.0", 38 | "hyperf/devtool": "3.0.*" 39 | }, 40 | "require-dev": { 41 | "friendsofphp/php-cs-fixer": "^3.9", 42 | "mockery/mockery": "^1.0", 43 | "phpstan/phpstan": "^0.12" 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "App\\": "app/" 48 | }, 49 | "files": [] 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "HyperfTest\\": "test/" 54 | } 55 | }, 56 | "minimum-stability": "dev", 57 | "prefer-stable": true, 58 | "config": { 59 | "optimize-autoloader": true, 60 | "sort-packages": true 61 | }, 62 | "extra": [], 63 | "scripts": { 64 | "post-root-package-install": [ 65 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 66 | ], 67 | "post-autoload-dump": [ 68 | "@php -r \"if(PHP_OS_FAMILY === 'Windows') shell_exec('del /f /s /q runtime\\container && rmdir runtime\\container'); else shell_exec('rm -rf runtime/container');\" " 69 | ], 70 | "analyse": "@php vendor/bin/phpstan analyse --memory-limit 512M -l 0 -c phpstan.neon ./app ./config", 71 | "cs-fix": "@php vendor/bin/php-cs-fixer fix $1", 72 | "start": "@php bin/hyperf.php start", 73 | "test": "@php vendor/bin/phpunit -c phpunit.xml --colors=always" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/config/autoload/annotations.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'paths' => [ 15 | BASE_PATH . '/app', 16 | ], 17 | 'ignore_annotations' => [ 18 | 'mixin', 19 | ], 20 | 'class_map' => [], 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /src/config/autoload/dependencies.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'amqp' => [ 15 | 'consumer' => [ 16 | 'namespace' => 'App\\Amqp\\Consumer', 17 | ], 18 | 'producer' => [ 19 | 'namespace' => 'App\\Amqp\\Producer', 20 | ], 21 | ], 22 | 'aspect' => [ 23 | 'namespace' => 'App\\Aspect', 24 | ], 25 | 'command' => [ 26 | 'namespace' => 'App\\Command', 27 | ], 28 | 'controller' => [ 29 | 'namespace' => 'App\\Controller', 30 | ], 31 | 'job' => [ 32 | 'namespace' => 'App\\Job', 33 | ], 34 | 'listener' => [ 35 | 'namespace' => 'App\\Listener', 36 | ], 37 | 'middleware' => [ 38 | 'namespace' => 'App\\Middleware', 39 | ], 40 | 'Process' => [ 41 | 'namespace' => 'App\\Processes', 42 | ], 43 | ], 44 | ]; 45 | -------------------------------------------------------------------------------- /src/config/autoload/logger.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'handler' => [ 15 | 'class' => Monolog\Handler\StreamHandler::class, 16 | 'constructor' => [ 17 | 'stream' => '/tmp/runtime/logs/hyperf.log', 18 | 'level' => Monolog\Logger::INFO, 19 | ], 20 | ], 21 | 'formatter' => [ 22 | 'class' => Monolog\Formatter\LineFormatter::class, 23 | 'constructor' => [ 24 | 'format' => null, 25 | 'dateFormat' => 'Y-m-d H:i:s', 26 | 'allowInlineLineBreaks' => true, 27 | ], 28 | ], 29 | 'processors' => [], 30 | ], 31 | ]; 32 | -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'box'), 17 | 'app_env' => env('APP_ENV', 'dev'), 18 | 'scan_cacheable' => env('SCAN_CACHEABLE', false), 19 | StdoutLoggerInterface::class => [ 20 | 'log_level' => [ 21 | LogLevel::ALERT, 22 | LogLevel::CRITICAL, 23 | LogLevel::EMERGENCY, 24 | LogLevel::ERROR, 25 | LogLevel::INFO, 26 | LogLevel::NOTICE, 27 | LogLevel::WARNING, 28 | ], 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /src/config/container.php: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | test 14 | 15 | 16 | 17 | 18 | app 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/test/Cases/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 29 | 30 | $res = $this->get('/'); 31 | 32 | $this->assertSame(0, $res['code']); 33 | $this->assertSame('Hello Hyperf.', $res['data']['message']); 34 | $this->assertSame('GET', $res['data']['method']); 35 | $this->assertSame('Hyperf', $res['data']['user']); 36 | 37 | $res = $this->get('/', ['user' => 'limx']); 38 | 39 | $this->assertSame(0, $res['code']); 40 | $this->assertSame('limx', $res['data']['user']); 41 | 42 | $res = $this->post('/', [ 43 | 'user' => 'limx', 44 | ]); 45 | $this->assertSame('Hello Hyperf.', $res['data']['message']); 46 | $this->assertSame('POST', $res['data']['method']); 47 | $this->assertSame('limx', $res['data']['user']); 48 | 49 | Context::set(AppendRequestIdProcessor::REQUEST_ID, $id = uniqid()); 50 | $pool = new Channel(1); 51 | di()->get(Coroutine::class)->create(function () use ($pool) { 52 | try { 53 | $all = Context::getContainer(); 54 | $pool->push((array) $all); 55 | } catch (\Throwable $exception) { 56 | $pool->push(false); 57 | } 58 | }); 59 | 60 | $data = $pool->pop(); 61 | $this->assertIsArray($data); 62 | $this->assertSame($id, $data[AppendRequestIdProcessor::REQUEST_ID]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/HttpTestCase.php: -------------------------------------------------------------------------------- 1 | client = make(Testing\Client::class); 35 | // $this->client = make(Testing\HttpClient::class, ['baseUri' => 'http://127.0.0.1:9501']); 36 | } 37 | 38 | public function __call($name, $arguments) 39 | { 40 | return $this->client->{$name}(...$arguments); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/bootstrap.php: -------------------------------------------------------------------------------- 1 | get(Hyperf\Contract\ApplicationInterface::class); 25 | --------------------------------------------------------------------------------