├── .gitattributes ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .php-cs-fixer.php ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README-CN.md ├── README.md ├── bin ├── composer-json-fixer ├── licenses-generator.sh ├── md-format └── translate.php ├── composer.json ├── example ├── index.php └── swow.php ├── phpstan.neon ├── phpunit.xml ├── rector.php └── src ├── App.php ├── BoundInterface.php ├── ConfigProvider.php ├── Constant.php ├── ContainerProxy.php ├── Factory ├── AppFactory.php ├── ClosureCommand.php ├── ClosureProcess.php ├── CommandFactory.php ├── CronFactory.php ├── ExceptionHandlerFactory.php ├── MiddlewareFactory.php ├── ParameterParser.php └── ProcessFactory.php └── Preset ├── Base.php ├── Default.php ├── Preset.php ├── SwooleCoroutine.php └── Swow.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /.travis export-ignore 2 | /docs export-ignore 3 | /tests export-ignore 4 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Release 8 | 9 | jobs: 10 | release: 11 | name: Release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Create Release 17 | id: create_release 18 | uses: actions/create-release@v1 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | tag_name: ${{ github.ref }} 23 | release_name: Release ${{ github.ref }} 24 | draft: false 25 | prerelease: false 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit 2 | 3 | on: [ push, pull_request ] 4 | 5 | env: 6 | SWOW_VERSION: 'develop' 7 | DAEMONIZE: '(true)' 8 | jobs: 9 | ci: 10 | name: Test PHP ${{ matrix.php }} on Swoole ${{ matrix.swoole }} 11 | runs-on: "${{ matrix.os }}" 12 | env: 13 | SWOOLE_VERSION: ${{ matrix.swoole }} 14 | strategy: 15 | matrix: 16 | os: [ ubuntu-latest ] 17 | php: [ '8.0', '8.1', '8.2' ] 18 | engine: [ 'swoole' ] 19 | swoole: [ 'v4.6.7', 'v4.7.1', 'v4.8.12', 'v5.0.3', 'master' ] 20 | exclude: 21 | - php: '8.1' 22 | swoole: 'v4.6.7' 23 | - php: '8.1' 24 | swoole: 'v4.7.1' 25 | - php: '8.2' 26 | swoole: 'v4.6.7' 27 | - php: '8.2' 28 | swoole: 'v4.7.1' 29 | - php: '8.2' 30 | swoole: 'v4.8.12' 31 | max-parallel: 16 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v2 35 | - name: Setup PHP 36 | uses: shivammathur/setup-php@v2 37 | with: 38 | php-version: ${{ matrix.php }} 39 | tools: phpize 40 | extensions: redis, pdo, pdo_mysql, bcmath 41 | ini-values: opcache.enable_cli=0, swoole.use_shortname='Off' 42 | coverage: none 43 | - name: Setup Swoole 44 | if: ${{ matrix.engine == 'swoole' }} 45 | run: | 46 | sudo apt-get clean 47 | sudo apt-get update 48 | sudo apt-get upgrade -f 49 | sudo apt-get install libcurl4-openssl-dev libc-ares-dev libpq-dev 50 | wget https://github.com/swoole/swoole-src/archive/${SWOOLE_VERSION}.tar.gz -O swoole.tar.gz 51 | mkdir -p swoole 52 | tar -xf swoole.tar.gz -C swoole --strip-components=1 53 | rm swoole.tar.gz 54 | cd swoole 55 | phpize 56 | if [ _${{ matrix.sw-version }} = '_v5.0.3' ] || [ _${{ matrix.sw-version }} = '_master' ] 57 | then 58 | ./configure --enable-openssl --enable-swoole-curl --enable-cares --enable-swoole-pgsql --enable-brotli 59 | else 60 | ./configure --enable-openssl --enable-http2 --enable-swoole-curl --enable-swoole-json 61 | fi 62 | make -j$(nproc) 63 | sudo make install 64 | sudo sh -c "echo extension=swoole > /etc/php/${{ matrix.php }}/cli/conf.d/swoole.ini" 65 | sudo sh -c "echo swoole.use_shortname='Off' >> /etc/php/${{ matrix.php }}/cli/conf.d/swoole.ini" 66 | php --ri swoole 67 | - name: Setup Swow 68 | if: ${{ matrix.engine == 'swow' }} 69 | run: | 70 | wget https://github.com/swow/swow/archive/"${SWOW_VERSION}".tar.gz -O swow.tar.gz 71 | mkdir -p swow 72 | tar -xf swow.tar.gz -C swow --strip-components=1 73 | rm swow.tar.gz 74 | cd swow/ext || exit 75 | 76 | phpize 77 | ./configure --enable-debug 78 | make -j "$(nproc)" 79 | sudo make install 80 | sudo sh -c "echo extension=swow > /etc/php/${{ matrix.php }}/cli/conf.d/swow.ini" 81 | php --ri swow 82 | - name: Setup Services 83 | run: docker run --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=true -d mysql:5.7 --bind-address=0.0.0.0 --default-authentication-plugin=mysql_native_password 84 | - name: Setup MySQL 85 | run: export TRAVIS_BUILD_DIR=$(pwd) && bash ./.travis/setup.mysql.sh 86 | - name: Setup Packages 87 | run: composer update -o --no-scripts 88 | - name: Run Server 89 | run: | 90 | php example/index.php start 91 | sleep 5 92 | - name: Run Analyse 93 | run: | 94 | composer analyse src 95 | - name: Run Test Cases 96 | run: | 97 | composer test 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .settings/ 3 | .project 4 | *.patch 5 | .idea/ 6 | .git/ 7 | codeCoverage/ 8 | vendor/ 9 | .phpintel/ 10 | .env 11 | .DS_Store 12 | *.lock 13 | node_modules/* 14 | vendor/friendsofphp/php-cs-fixer/tests/AutoReview/ 15 | vendor/friendsofphp/php-cs-fixer/tests/Cache/ 16 | vendor/friendsofphp/php-cs-fixer/tests/ConfigurationException/ 17 | vendor/friendsofphp/php-cs-fixer/tests/Console/ 18 | vendor/friendsofphp/php-cs-fixer/tests/Differ/ 19 | vendor/friendsofphp/php-cs-fixer/tests/DocBlock/ 20 | vendor/friendsofphp/php-cs-fixer/tests/Doctrine/ 21 | vendor/friendsofphp/php-cs-fixer/tests/Error/ 22 | vendor/friendsofphp/php-cs-fixer/tests/Event/ 23 | vendor/friendsofphp/php-cs-fixer/tests/Fixer/ 24 | vendor/friendsofphp/php-cs-fixer/tests/FixerConfiguration/ 25 | vendor/friendsofphp/php-cs-fixer/tests/FixerDefinition/ 26 | vendor/friendsofphp/php-cs-fixer/tests/Indicator/ 27 | vendor/friendsofphp/php-cs-fixer/tests/Linter/ 28 | vendor/friendsofphp/php-cs-fixer/tests/Report/ 29 | vendor/friendsofphp/php-cs-fixer/tests/Runner/ 30 | vendor/friendsofphp/php-cs-fixer/tests/Smoke/ 31 | vendor/friendsofphp/php-cs-fixer/tests/Tokenizer/ 32 | Would skip repository vendor/swoole/ide-helper 33 | .phpunit.result.cache 34 | -------------------------------------------------------------------------------- /.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 | 'global_namespace_import' => [ 35 | 'import_classes' => true, 36 | 'import_constants' => true, 37 | 'import_functions' => null, 38 | ], 39 | 'blank_line_before_statement' => [ 40 | 'statements' => [ 41 | 'declare', 42 | ], 43 | ], 44 | 'general_phpdoc_annotation_remove' => [ 45 | 'annotations' => [ 46 | 'author' 47 | ], 48 | ], 49 | 'ordered_imports' => [ 50 | 'imports_order' => [ 51 | 'class', 'function', 'const', 52 | ], 53 | 'sort_algorithm' => 'alpha', 54 | ], 55 | 'single_line_comment_style' => [ 56 | 'comment_types' => [ 57 | ], 58 | ], 59 | 'yoda_style' => [ 60 | 'always_move_variable' => false, 61 | 'equal' => false, 62 | 'identical' => false, 63 | ], 64 | 'phpdoc_align' => [ 65 | 'align' => 'left', 66 | ], 67 | 'multiline_whitespace_before_semicolons' => [ 68 | 'strategy' => 'no_multi_line', 69 | ], 70 | 'constant_case' => [ 71 | 'case' => 'lower', 72 | ], 73 | 'class_attributes_separation' => true, 74 | 'combine_consecutive_unsets' => true, 75 | 'declare_strict_types' => true, 76 | 'linebreak_after_opening_tag' => true, 77 | 'lowercase_static_reference' => true, 78 | 'no_useless_else' => true, 79 | 'no_unused_imports' => true, 80 | 'not_operator_with_successor_space' => true, 81 | 'not_operator_with_space' => false, 82 | 'ordered_class_elements' => true, 83 | 'php_unit_strict' => false, 84 | 'phpdoc_separation' => false, 85 | 'single_quote' => true, 86 | 'standardize_not_equals' => true, 87 | 'multiline_comment_opening_closing' => true, 88 | 'single_line_empty_body' => false, 89 | ]) 90 | ->setFinder( 91 | PhpCsFixer\Finder::create() 92 | ->exclude('bin') 93 | ->exclude('public') 94 | ->exclude('runtime') 95 | ->exclude('vendor') 96 | ->in(__DIR__) 97 | ) 98 | ->setUsingCache(false); 99 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: required 4 | 5 | matrix: 6 | include: 7 | - php: 7.2 8 | env: SW_VERSION="4.5.4" DAEMONIZE=1 9 | - php: 7.3 10 | env: SW_VERSION="4.5.4" DAEMONIZE=1 11 | - php: 7.4 12 | env: SW_VERSION="4.5.4" DAEMONIZE=1 13 | 14 | services: 15 | - mysql 16 | - redis 17 | - docker 18 | 19 | before_install: 20 | - export PHP_MAJOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 1)" 21 | - export PHP_MINOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 2)" 22 | - echo $PHP_MAJOR 23 | - echo $PHP_MINOR 24 | 25 | install: 26 | - cd $TRAVIS_BUILD_DIR 27 | - bash .travis/swoole.install.sh 28 | - phpenv config-rm xdebug.ini || echo "xdebug not available" 29 | - phpenv config-add .travis/ci.ini 30 | - bash .travis/setup.mysql.sh 31 | 32 | before_script: 33 | - cd $TRAVIS_BUILD_DIR 34 | - composer config -g process-timeout 900 && composer update 35 | - php example/index.php start 36 | - sleep 5 37 | 38 | script: 39 | - composer analyse src 40 | 41 | notifications: 42 | webhooks: https://oapi.dingtalk.com/robot/send?access_token=72c12e591c435f0f41e09261f6252aeafd284e432657b1f8d4a77b5aac8fbfcd 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.0.3 - TBD 2 | 3 | ## Fixed 4 | 5 | - [#18](https://github.com/hyperf/nano/pull/18) Fixed custom dependencies do not work. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Hyperf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | [English](./README.md) | 中文 2 | 3 |

4 | 5 | Financial Contributors on Open Collective 6 | Php Version 7 | Swoole Version 8 | Nano License 9 |

10 | 11 | # Nano, by Hyperf 12 | 13 | Nano 是一款零配置、无骨架、极小化的 Hyperf 发行版,通过 Nano 可以让您仅仅通过 1 个 PHP 文件即可快速搭建一个 Hyperf 应用。 14 | 15 | ## 设计理念 16 | 17 | `Svelte` 的作者提出过一个论断:“框架不是用来组织代码的,是用来组织思路的”。而 Nano 最突出的一个优点就是不打断你的思路。Nano 非常擅长于自我声明,几乎不需要了解框架细节,只需要简单读一读代码,就能知道代码的目的。通过极简的代码声明,完成一个完整的 Hyperf 应用。 18 | 19 | ## 特性 20 | 21 | * 无骨架 22 | * 零配置 23 | * 快速启动 24 | * 闭包风格 25 | * 支持注解外的全部 Hyperf 功能 26 | * 兼容全部 Hyperf 组件 27 | * Phar 友好 28 | 29 | ## 安装 30 | 31 | ```bash 32 | composer require hyperf/nano 33 | ``` 34 | 35 | ## 快速开始 36 | 37 | 创建一个 PHP 文件,如 index.php 如下: 38 | 39 | ```php 40 | get('/', function () { 48 | 49 | $user = $this->request->input('user', 'nano'); 50 | $method = $this->request->getMethod(); 51 | 52 | return [ 53 | 'message' => "hello {$user}", 54 | 'method' => $method, 55 | ]; 56 | 57 | }); 58 | 59 | $app->run(); 60 | ``` 61 | 62 | 启动服务: 63 | 64 | ```bash 65 | php index.php start 66 | ``` 67 | 68 | 简洁如此。 69 | 70 | ## 更多示例 71 | 72 | ### 路由 73 | 74 | `$app` 集成了 Hyperf 路由器的所有方法。 75 | 76 | ```php 77 | addGroup('/nano', function () use ($app) { 85 | $app->addRoute(['GET', 'POST'], '/{id:\d+}', function($id) { 86 | return '/nano/'.$id; 87 | }); 88 | $app->put('/{name:.+}', function($name) { 89 | return '/nano/'.$name; 90 | }); 91 | }); 92 | 93 | $app->run(); 94 | ``` 95 | 96 | ### DI 容器 97 | ```php 98 | getContainer()->set(Foo::class, new Foo()); 112 | 113 | $app->get('/', function () { 114 | /** @var ContainerProxy $this */ 115 | $foo = $this->get(Foo::class); 116 | return $foo->bar(); 117 | }); 118 | 119 | $app->run(); 120 | ``` 121 | > 所有 $app 管理的闭包回调中,$this 都被绑定到了 `Hyperf\Nano\ContainerProxy` 上。 122 | 123 | ### 中间件 124 | ```php 125 | get('/', function () { 133 | return $this->request->getAttribute('key'); 134 | }); 135 | 136 | $app->addMiddleware(function ($request, $handler) { 137 | $request = $request->withAttribute('key', 'value'); 138 | return $handler->handle($request); 139 | }); 140 | 141 | $app->run(); 142 | ``` 143 | 144 | > 除了闭包之外,所有 $app->addXXX() 方法还接受类名作为参数。可以传入对应的 Hyperf 类。 145 | 146 | ### 异常处理 147 | 148 | ```php 149 | get('/', function () { 158 | throw new \Exception(); 159 | }); 160 | 161 | $app->addExceptionHandler(function ($throwable, $response) { 162 | return $response->withStatus('418') 163 | ->withBody(new SwooleStream('I\'m a teapot')); 164 | }); 165 | 166 | $app->run(); 167 | ``` 168 | 169 | ### 命令行 170 | 171 | ```php 172 | addCommand('echo {--name=Nano}', function($name){ 181 | $this->output->info("Hello, {$name}"); 182 | })->setDescription('The echo command.'); 183 | 184 | $app->run(); 185 | ``` 186 | 187 | 执行 188 | 189 | ```bash 190 | php index.php echo 191 | ``` 192 | 193 | ### 事件监听 194 | 195 | ```php 196 | addListener(BootApplication::class, function($event){ 206 | $this->get(StdoutLoggerInterface::class)->info('App started'); 207 | }); 208 | 209 | $app->run(); 210 | ``` 211 | 212 | ### 自定义进程 213 | 214 | ```php 215 | addProcess(function(){ 224 | while (true) { 225 | sleep(1); 226 | $this->container->get(StdoutLoggerInterface::class)->info('Processing...'); 227 | } 228 | })->setName('nano-process')->setNums(1); 229 | 230 | $app->addProcess(function(){ 231 | $this->container->get(StdoutLoggerInterface::class)->info('根据env判定是否需要启动进程...'); 232 | })->setName('nano-process')->setNums(1)->setEnable(\Hyperf\Support\env('PROCESS_ENABLE', true)))); 233 | 234 | $app->run(); 235 | ``` 236 | 237 | ### 定时任务 238 | 239 | ```php 240 | addCrontab('* * * * * *', function(){ 249 | $this->get(StdoutLoggerInterface::class)->info('execute every second!'); 250 | })->setName('nano-crontab')->setOnOneServer(true)->setMemo('Test crontab.'); 251 | 252 | $app->run(); 253 | ``` 254 | 255 | ### AMQP 256 | 257 | ```php 258 | payload = $data; 274 | } 275 | } 276 | 277 | $app = AppFactory::createBase(); 278 | $container = $app->getContainer(); 279 | 280 | $app->config([ 281 | 'amqp' => [ 282 | 'default' => [ 283 | 'host' => 'localhost', 284 | 'port' => 5672, 285 | 'user' => 'guest', 286 | 'password' => 'guest', 287 | 'vhost' => '/', 288 | 'concurrent' => [ 289 | 'limit' => 1, 290 | ], 291 | 'pool' => [ 292 | 'min_connections' => 1, 293 | 'max_connections' => 10, 294 | 'connect_timeout' => 10.0, 295 | 'wait_timeout' => 3.0, 296 | 'heartbeat' => -1, 297 | ], 298 | 'params' => [ 299 | 'insist' => false, 300 | 'login_method' => 'AMQPLAIN', 301 | 'login_response' => null, 302 | 'locale' => 'en_US', 303 | 'connection_timeout' => 3.0, 304 | 'read_write_timeout' => 6.0, 305 | 'context' => null, 306 | 'keepalive' => false, 307 | 'heartbeat' => 3, 308 | 'close_on_destruct' => true, 309 | ], 310 | ], 311 | ], 312 | ]); 313 | 314 | $app->addProcess(function () { 315 | $message = new class extends Amqp\Message\ConsumerMessage { 316 | protected $exchange = 'hyperf'; 317 | 318 | protected $queue = 'hyperf'; 319 | 320 | protected $routingKey = 'hyperf'; 321 | 322 | public function consumeMessage($data, \PhpAmqpLib\Message\AMQPMessage $message): string 323 | { 324 | var_dump($data); 325 | return Amqp\Result::ACK; 326 | } 327 | }; 328 | $consumer = $this->get(Amqp\Consumer::class); 329 | $consumer->consume($message); 330 | }); 331 | 332 | $app->get('/', function () { 333 | /** @var Amqp\Producer $producer */ 334 | $producer = $this->get(Amqp\Producer::class); 335 | $producer->produce(new Message(['id' => $id = uniqid()])); 336 | return $this->response->json([ 337 | 'id' => $id, 338 | 'message' => 'Hello World.' 339 | ]); 340 | }); 341 | 342 | $app->run(); 343 | 344 | ``` 345 | 346 | ### 使用更多 Hyperf 组件 347 | 348 | ```php 349 | config([ 358 | 'db.default' => [ 359 | 'host' => env('DB_HOST', 'localhost'), 360 | 'port' => env('DB_PORT', 3306), 361 | 'database' => env('DB_DATABASE', 'hyperf'), 362 | 'username' => env('DB_USERNAME', 'root'), 363 | 'password' => env('DB_PASSWORD', ''), 364 | ] 365 | ]); 366 | 367 | $app->get('/', function(){ 368 | return DB::query('SELECT * FROM `user` WHERE gender = ?;', [1]); 369 | }); 370 | 371 | $app->run(); 372 | ``` 373 | 374 | 375 | ### 热更新 376 | 377 | 热更新组件 [hyperf/watcher](https://github.com/hyperf/watcher) 在 `v2.2.6` 版本开始,支持 `Nano` 热更新。 378 | 379 | 首先我们需要引入组件 380 | 381 | ```shell 382 | composer require hyperf/watcher 383 | ``` 384 | 385 | 接下来编写样例代码 `index.php` 386 | 387 | ```php 388 | config([ 397 | 'server.settings.pid_file' => BASE_PATH . '/hyperf.pid', 398 | 'watcher' => [ 399 | 'driver' => ScanFileDriver::class, 400 | 'bin' => 'php', 401 | 'command' => 'index.php start', 402 | 'watch' => [ 403 | 'dir' => [], 404 | 'file' => ['index.php'], 405 | 'scan_interval' => 2000, 406 | ], 407 | ], 408 | ]); 409 | 410 | $app->get('/', function () { 411 | 412 | $user = $this->request->input('user', 'nano'); 413 | $method = $this->request->getMethod(); 414 | 415 | return [ 416 | 'message' => "Hello {$user}", 417 | 'method' => $method, 418 | ]; 419 | 420 | }); 421 | 422 | $app->run(); 423 | ``` 424 | 425 | 启动服务 426 | 427 | ```shell 428 | $ php index.php server:watch 429 | ``` 430 | 431 | 最后我们只需要修改 `index.php` 的源码,就可以看到具体效果了。 432 | 433 | ### 如何使用 Swow 434 | 435 | - 安装兼容层 436 | 437 | ```shell 438 | composer require "hyperf/engine-swow:^2.0" 439 | ``` 440 | 441 | - 运行代码 442 | 443 | ```php 444 | get('/', function () { 455 | return 'Hello World'; 456 | }); 457 | 458 | $app->run(); 459 | ``` 460 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [中文](./README-CN.md) 2 | 3 |

4 | 5 | Financial Contributors on Open Collective 6 | Php Version 7 | Swoole Version 8 | Nano License 9 |

10 | 11 | # Nano, by Hyperf 12 | 13 | Nano is a zero-config, no skeleton, minimal Hyperf distribution that allows you to quickly build a Hyperf application with just a single PHP file. 14 | 15 | ## Purpose 16 | 17 | The author of `Svelte` has said that "Frameworks are not tools for organizing your code, they are tools for organizing your mind". The biggest advantages of Nano is that it doesn't interrupt your mind of thought. The Nano is good at self-declaration that you have no need to know the details of the framework. You can read the code fastly and know what it's for. Write a complete Hyperf application with minimal code declarations. 18 | 19 | 20 | ## Feature 21 | 22 | * No skeleton. 23 | * Fast startup. 24 | * Zero config. 25 | * Closure style. 26 | * Support all Hyperf features except annotations. 27 | * Compatible with all Hyperf components. 28 | 29 | ## Example 30 | 31 | Create a single PHP file, like `index.php`: 32 | 33 | ```php 34 | get('/', function () { 42 | 43 | $user = $this->request->input('user', 'nano'); 44 | $method = $this->request->getMethod(); 45 | 46 | return [ 47 | 'message' => "hello {$user}", 48 | 'method' => $method, 49 | ]; 50 | 51 | }); 52 | 53 | $app->run(); 54 | ``` 55 | 56 | Run the server: 57 | 58 | ```bash 59 | php index.php start 60 | ``` 61 | 62 | That's all you need. 63 | 64 | ## More Examples 65 | 66 | ### Routing 67 | 68 | `$app` inherits all methods from hyperf router. 69 | 70 | ```php 71 | addGroup('/nano', function () use ($app) { 79 | $app->addRoute(['GET', 'POST'], '/{id:\d+}', function($id) { 80 | return '/nano/'.$id; 81 | }); 82 | $app->put('/{name:.+}', function($name) { 83 | return '/nano/'.$name; 84 | }); 85 | }); 86 | 87 | $app->run(); 88 | ``` 89 | 90 | ### DI Container 91 | ```php 92 | getContainer()->set(Foo::class, new Foo()); 106 | 107 | $app->get('/', function () { 108 | /** @var ContainerProxy $this */ 109 | $foo = $this->get(Foo::class); 110 | return $foo->bar(); 111 | }); 112 | 113 | $app->run(); 114 | ``` 115 | > As a convention, $this is bind to ContainerProxy in all closures managed by nano, including middleware, exception handler and more. 116 | 117 | ### Middleware 118 | ```php 119 | get('/', function () { 127 | return $this->request->getAttribute('key'); 128 | }); 129 | 130 | $app->addMiddleware(function ($request, $handler) { 131 | $request = $request->withAttribute('key', 'value'); 132 | return $handler->handle($request); 133 | }); 134 | 135 | $app->run(); 136 | ``` 137 | 138 | > In addition to closure, all $app->addXXX() methods also accept class name as argument. You can pass any corresponding hyperf classes. 139 | 140 | ### ExceptionHandler 141 | 142 | ```php 143 | get('/', function () { 152 | throw new \Exception(); 153 | }); 154 | 155 | $app->addExceptionHandler(function ($throwable, $response) { 156 | return $response->withStatus('418') 157 | ->withBody(new SwooleStream('I\'m a teapot')); 158 | }); 159 | 160 | $app->run(); 161 | ``` 162 | 163 | ### Custom Command 164 | 165 | ```php 166 | addCommand('echo {--name=Nano}', function($name){ 175 | $this->output->info("Hello, {$name}"); 176 | })->setDescription('The echo command.'); 177 | 178 | $app->run(); 179 | ``` 180 | 181 | To run this command, execute 182 | ```bash 183 | php index.php echo 184 | ``` 185 | 186 | ### Event Listener 187 | ```php 188 | addListener(BootApplication::class, function($event){ 198 | $this->get(StdoutLoggerInterface::class)->info('App started'); 199 | }); 200 | 201 | $app->run(); 202 | ``` 203 | 204 | ### Custom Process 205 | ```php 206 | addProcess(function(){ 215 | while (true) { 216 | sleep(1); 217 | $this->container->get(StdoutLoggerInterface::class)->info('Processing...'); 218 | } 219 | })->setName('nano-process')->setNums(1); 220 | 221 | $app->addProcess(function(){ 222 | $this->container->get(StdoutLoggerInterface::class)->info('Determine whether the process needs to be started based on env...'); 223 | })->setName('nano-process')->setNums(1)->setEnable(\Hyperf\Support\env('PROCESS_ENABLE', true)))); 224 | 225 | $app->run(); 226 | ``` 227 | 228 | ### Crontab 229 | 230 | ```php 231 | addCrontab('* * * * * *', function(){ 240 | $this->get(StdoutLoggerInterface::class)->info('execute every second!'); 241 | })->setName('nano-crontab')->setOnOneServer(true)->setMemo('Test crontab.'); 242 | 243 | $app->run(); 244 | ``` 245 | 246 | ### Use Hyperf Component 247 | 248 | ```php 249 | config([ 258 | 'db.default' => [ 259 | 'host' => env('DB_HOST', 'localhost'), 260 | 'port' => env('DB_PORT', 3306), 261 | 'database' => env('DB_DATABASE', 'hyperf'), 262 | 'username' => env('DB_USERNAME', 'root'), 263 | 'password' => env('DB_PASSWORD', ''), 264 | ] 265 | ]); 266 | 267 | $app->get('/', function(){ 268 | return DB::query('SELECT * FROM `user` WHERE gender = ?;', [1]); 269 | }); 270 | 271 | $app->run(); 272 | ``` 273 | 274 | ### How to use Swow 275 | 276 | - require swow engine 277 | 278 | ```shell 279 | composer require "hyperf/engine-swow:^2.0" 280 | ``` 281 | 282 | - run the code 283 | 284 | ```php 285 | get('/', function () { 296 | return 'Hello World'; 297 | }); 298 | 299 | $app->run(); 300 | ``` 301 | -------------------------------------------------------------------------------- /bin/composer-json-fixer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | in(__DIR__ . '/../src') 15 | ->name('composer.json') 16 | ->files(); 17 | 18 | $require = []; 19 | $autoload = []; 20 | $autoloadFiles = []; 21 | $autoloadDev = []; 22 | $configProviders = []; 23 | foreach ($files as $file) { 24 | $component = basename(dirname($file)); 25 | $composerJson = json_decode(file_get_contents($file), true); 26 | 27 | foreach ($composerJson['autoload']['files'] ?? [] as $file) { 28 | $autoloadFiles[] = "src/{$component}/" . preg_replace('#^./#', '', $file); 29 | } 30 | foreach ($composerJson['autoload']['psr-4'] ?? [] as $ns => $dir) { 31 | $autoload[$ns] = "src/{$component}/" . trim($dir, '/') . '/'; 32 | } 33 | foreach ($composerJson['autoload-dev']['psr-4'] ?? [] as $ns => $dir) { 34 | $autoloadDev[$ns] = "src/{$component}/" . trim($dir, '/') . '/'; 35 | } 36 | if (isset($composerJson['extra']['hyperf']['config'])) { 37 | $configProviders = array_merge($configProviders, (array)$composerJson['extra']['hyperf']['config']); 38 | } 39 | } 40 | 41 | ksort($autoload); 42 | sort($autoloadFiles); 43 | ksort($autoloadDev); 44 | sort($configProviders); 45 | 46 | $json = json_decode(file_get_contents(__DIR__ . '/../composer.json')); 47 | $json->autoload->files = $autoloadFiles; 48 | $json->autoload->{'psr-4'} = $autoload; 49 | $json->{'autoload-dev'}->{'psr-4'} = $autoloadDev; 50 | $json->extra->hyperf->config = $configProviders; 51 | 52 | file_put_contents( 53 | __DIR__ . '/../composer.json', 54 | json_encode($json, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL 55 | ); 56 | -------------------------------------------------------------------------------- /bin/licenses-generator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -e 3 | # execute `composer global require comcast/php-legal-licenses` before run this shell 4 | 5 | NOW=$(date +%s) 6 | BASEPATH=$(cd `dirname $0`; cd ../src/; pwd) 7 | 8 | repos=$(ls "$BASEPATH") 9 | echo $NOW 10 | 11 | function generate() { 12 | if [ -f "composer.json" ]; then 13 | composer update -q 14 | php-legal-licenses generate 15 | fi 16 | if [ -f "composer.lock" ]; then 17 | rm -rf ./composer.lock 18 | fi 19 | if [ -d "vendor" ]; then 20 | rm -rf ./vendor 21 | fi 22 | if [ -f "licenses.md" ]; then 23 | git add ./licenses.md 24 | fi 25 | } 26 | 27 | cd .. 28 | 29 | git checkout master && git checkout -b licenses-generate-"$NOW" 30 | 31 | echo "Generating main repository"; 32 | generate 33 | 34 | cd ./src 35 | 36 | for REPO in $repos 37 | do 38 | echo "Generating $REPO"; 39 | cd "./$REPO" 40 | 41 | generate 42 | 43 | cd ../ 44 | 45 | done 46 | 47 | git commit -m "Update licenses.md" 48 | 49 | TIME=$(echo "$(date +%s) - $NOW" | bc) 50 | 51 | printf "Execution time: %f seconds" "$TIME" -------------------------------------------------------------------------------- /bin/md-format: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | in(__DIR__ . '/../docs/zh-cn') 15 | ->name('*.md') 16 | ->files(); 17 | 18 | foreach ($files as $file) { 19 | file_put_contents($file, replace(file_get_contents($file))); 20 | } 21 | echo count($files).' markdown files formatted!'.PHP_EOL; 22 | 23 | function replace($text) 24 | { 25 | $cjk = '' . 26 | '\x{2e80}-\x{2eff}' . 27 | '\x{2f00}-\x{2fdf}' . 28 | '\x{3040}-\x{309f}' . 29 | '\x{30a0}-\x{30ff}' . 30 | '\x{3100}-\x{312f}' . 31 | '\x{3200}-\x{32ff}' . 32 | '\x{3400}-\x{4dbf}' . 33 | '\x{4e00}-\x{9fff}' . 34 | '\x{f900}-\x{faff}'; 35 | 36 | $patterns = [ 37 | 'cjk_quote' => [ 38 | '([' . $cjk . '])(["\'])', 39 | '$1 $2', 40 | ], 41 | 42 | 'quote_cjk' => [ 43 | '(["\'])([' . $cjk . '])', 44 | '$1 $2', 45 | ], 46 | 47 | 'fix_quote' => [ 48 | '(["\']+)(\s*)(.+?)(\s*)(["\']+)', 49 | '$1$3$5', 50 | ], 51 | 52 | 'cjk_operator_ans' => [ 53 | '([' . $cjk . '])([A-Za-zΑ-Ωα-ω0-9])([\+\-\*\/=&\\|<>])', 54 | '$1 $2 $3', 55 | ], 56 | 57 | 'bracket_cjk' => [ 58 | '([' . $cjk . '])([`]+\w(.*?)\w[`]+)([' . $cjk . ',。])', 59 | '$1 $2 $4', 60 | ], 61 | 62 | 'ans_operator_cjk' => [ 63 | '([\+\-\*\/=&\\|<>])([A-Za-zΑ-Ωα-ω0-9])([' . $cjk . '])', 64 | '$1 $2 $3', 65 | ], 66 | 67 | 'cjk_ans' => [ 68 | '([' . $cjk . '])([A-Za-zΑ-Ωα-ω0-9@&%\=\$\^\\-\+\\\><])', 69 | '$1 $2', 70 | ], 71 | 72 | 'ans_cjk' => [ 73 | '([A-Za-zΑ-Ωα-ω0-9~!%&=;\,\.\?\$\^\\-\+\\\<>])([' . $cjk . '])', 74 | '$1 $2', 75 | ], 76 | ]; 77 | $code = []; 78 | $i = 0; 79 | $text = preg_replace_callback('/```(\n|.)*?\n```/m', function ($match) use (&$code, &$i) { 80 | $code[++$i] = $match[0]; 81 | return "__REPLACEMARK__{$i}__"; 82 | }, $text); 83 | foreach ($patterns as $key => $value) { 84 | $text = preg_replace('/' . $value[0] . '/iu', $value[1], $text); 85 | } 86 | $text = preg_replace_callback('/__REPLACEMARK__(\d+)__/s', function ($match) use ($code) { 87 | return $code[$match[1]]; 88 | }, $text); 89 | 90 | return $text; 91 | } -------------------------------------------------------------------------------- /bin/translate.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'targetDir' => BASE_PATH . '/docs/zh-tw/', 16 | 'rule' => 's2twp.json', 17 | ], 18 | 'zh-hk' => [ 19 | 'targetDir' => BASE_PATH . '/docs/zh-hk/', 20 | 'rule' => 's2hk.json', 21 | ], 22 | ]; 23 | 24 | $finder = new Finder(); 25 | $finder->files()->in(BASE_PATH . '/docs/zh-cn'); 26 | 27 | foreach ($config as $key => $item) { 28 | $od = opencc_open($item['rule']); 29 | foreach ($finder as $fileInfo) { 30 | $targetPath = $item['targetDir'] . $fileInfo->getRelativePath(); 31 | $isCreateDir = false; 32 | if (! is_dir($targetPath)) { 33 | mkdir($targetPath, 0777, true); 34 | chmod($targetPath, 0777); 35 | $isCreateDir = true; 36 | } 37 | if (! is_writable($targetPath)) { 38 | echo sprintf('Target path %s is not writable.' . PHP_EOL, $targetPath); 39 | } 40 | if ($fileInfo->getExtension() === 'md') { 41 | $translated = opencc_convert($fileInfo->getContents(), $od); 42 | $translated = str_replace('](zh-cn/', '](' . $key . '/', $translated); 43 | $targetTranslatedPath = $item['targetDir'] . $fileInfo->getRelativePathname(); 44 | @file_put_contents($targetTranslatedPath, $translated); 45 | } else { 46 | $targetTranslatedPath = $item['targetDir'] . $fileInfo->getRelativePathname(); 47 | @copy($fileInfo->getRealPath(), $targetTranslatedPath); 48 | } 49 | } 50 | opencc_close($od); 51 | } 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf/nano", 3 | "type": "project", 4 | "license": "MIT", 5 | "keywords": [ 6 | "php", 7 | "hyperf", 8 | "nano" 9 | ], 10 | "description": "Scale Hyperf application down to a single file", 11 | "autoload": { 12 | "psr-4": { 13 | "Hyperf\\Nano\\": "src/" 14 | } 15 | }, 16 | "autoload-dev": { 17 | "psr-4": { 18 | "HyperfTest\\Nano\\": "tests/" 19 | } 20 | }, 21 | "require": { 22 | "php": ">=8.0", 23 | "hyperf/command": "^3.0.0", 24 | "hyperf/config": "^3.0.0", 25 | "hyperf/context": "^3.0.16", 26 | "hyperf/contract": "^3.0.0", 27 | "hyperf/di": "^3.0.0", 28 | "hyperf/framework": "^3.0.0", 29 | "hyperf/http-server": "^3.0.0", 30 | "hyperf/stringable": "^3.0.0", 31 | "hyperf/support": "^3.0.0", 32 | "hyperf/tappable": "^3.0.0" 33 | }, 34 | "require-dev": { 35 | "friendsofphp/php-cs-fixer": "^3.0", 36 | "hyperf/crontab": "^3.0.0", 37 | "hyperf/db": "^3.0.0", 38 | "hyperf/guzzle": "^3.0.0", 39 | "hyperf/process": "^3.0.0", 40 | "hyperf/testing": "^3.0.0", 41 | "phpstan/phpstan": "^1.0", 42 | "phpunit/phpunit": ">=7.0", 43 | "swoole/ide-helper": "dev-master", 44 | "symfony/finder": "^4.0|^5.0" 45 | }, 46 | "suggest": { 47 | "hyperf/crontab": "Required to use closure crontab", 48 | "hyperf/process": "Required to use closure process" 49 | }, 50 | "minimum-stability": "dev", 51 | "prefer-stable": true, 52 | "config": { 53 | "optimize-autoloader": true, 54 | "sort-packages": true 55 | }, 56 | "scripts": { 57 | "test": "co-phpunit -c phpunit.xml --colors=always", 58 | "analyse": "phpstan analyse --memory-limit=-1 -l 5 -c phpstan.neon", 59 | "cs-fix": "php-cs-fixer fix $1" 60 | }, 61 | "extra": { 62 | "branch-alias": { 63 | "dev-master": "2.0-dev" 64 | }, 65 | "hyperf": { 66 | "config": "Hyperf\\Nano\\ConfigProvider" 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /example/index.php: -------------------------------------------------------------------------------- 1 | Foo::class, 42 | ]); 43 | 44 | $app->config([ 45 | 'server' => [ 46 | 'settings' => [ 47 | 'daemonize' => (int) env('DAEMONIZE', 0), 48 | ], 49 | ], 50 | ]); 51 | 52 | $app->get('/', function () { 53 | $user = $this->request->input('user', 'nano'); 54 | $method = $this->request->getMethod(); 55 | return [ 56 | 'message' => "hello {$user}", 57 | 'method' => $method, 58 | ]; 59 | }); 60 | 61 | $app->addGroup('/route', function () use ($app) { 62 | $app->addRoute(['GET', 'POST'], '/{id:\d+}', function ($id) { 63 | return '/route/' . $id; 64 | }); 65 | $app->put('/{name:.+}', function ($name) { 66 | return '/route/' . $name; 67 | }); 68 | }); 69 | 70 | $app->get('/di', function () { 71 | /** @var ContainerProxy $this */ 72 | $foo = $this->get(Foo::class); 73 | return $foo->bar(); 74 | }); 75 | 76 | $app->get('/foo', function () { 77 | /* @var ContainerProxy $this */ 78 | return $this->get(FooInterface::class)->bar(); 79 | }); 80 | 81 | $app->get('/middleware', function () { 82 | return $this->request->getAttribute('key'); 83 | }); 84 | 85 | $app->addMiddleware(function ($request, $handler) { 86 | $request = $request->withAttribute('key', 'value'); 87 | return $handler->handle($request); 88 | }); 89 | 90 | $app->get('/exception', function () { 91 | throw new Exception(); 92 | }); 93 | 94 | $app->addExceptionHandler(function ($throwable, $response) { 95 | return $response->withStatus('418')->withBody(new SwooleStream('I\'m a teapot')); 96 | }); 97 | 98 | $app->addCommand('echo {--name=Nano}', function ($name) { 99 | /* @var Command $this */ 100 | $this->output->info("Hello, {$name}!"); 101 | })->setDescription('The echo command.'); 102 | 103 | $app->addListener(BootApplication::class, function ($event) { 104 | $this->get(StdoutLoggerInterface::class)->info('App started'); 105 | }); 106 | 107 | $app->addProcess(function () { 108 | $name = $this->name; 109 | while (true) { 110 | sleep(1); 111 | $this->container->get(StdoutLoggerInterface::class)->info("{$name} Processing..."); 112 | } 113 | })->setName('nano-process')->setEnable(fn ($server) => true); 114 | 115 | $app->addCrontab('* * * * * *', function () { 116 | $this->get(StdoutLoggerInterface::class)->info('execute every second!'); 117 | })->setName('nano-crontab'); 118 | 119 | $app->config([ 120 | 'db.default' => [ 121 | 'host' => env('DB_HOST', 'localhost'), 122 | 'port' => env('DB_PORT', 3306), 123 | 'database' => env('DB_DATABASE', 'hyperf'), 124 | 'username' => env('DB_USERNAME', 'root'), 125 | 'password' => env('DB_PASSWORD', ''), 126 | ], 127 | ]); 128 | 129 | $app->get('/db', function () { 130 | return DB::query('SELECT * FROM `user` WHERE gender = ?;', [1]); 131 | }); 132 | 133 | $app->run(); 134 | -------------------------------------------------------------------------------- /example/swow.php: -------------------------------------------------------------------------------- 1 | get('/', function () { 19 | return 'Hello World'; 20 | }); 21 | 22 | $app->run(); 23 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | # Magic behaviour with __get, __set, __call and __callStatic is not exactly static analyser-friendly :) 2 | # Fortunately, You can ingore it by the following config. 3 | # 4 | # vendor/bin/phpstan analyse app --memory-limit 200M -l 0 5 | # 6 | parameters: 7 | bootstrapFiles: 8 | - "vendor/autoload.php" 9 | inferPrivatePropertyTypeFromConstructor: true 10 | treatPhpDocTypesAsCertain: true 11 | reportUnmatchedIgnoredErrors: false 12 | ignoreErrors: 13 | - '#Static call to instance method Hyperf\\HttpServer\\Router\\Router::[a-zA-Z0-9\\_]+\(\)#' 14 | - '#Static call to instance method Hyperf\\DbConnection\\Db::[a-zA-Z0-9\\_]+\(\)#' 15 | - '#Property Hyperf\\Nano\\ContainerProxy::\$(request|response) is never read, only written.#' 16 | - '#Constant BASE_PATH not found.#' 17 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | tests 14 | 15 | 16 | 17 | 18 | src 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 18 | __DIR__ . '/src', 19 | ]); 20 | 21 | // register a single rule 22 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 23 | 24 | // define sets of rules 25 | $rectorConfig->sets([ 26 | LevelSetList::UP_TO_PHP_80, 27 | ]); 28 | }; 29 | -------------------------------------------------------------------------------- /src/App.php: -------------------------------------------------------------------------------- 1 | config = $this->container->get(ConfigInterface::class); 56 | $this->dispatcherFactory = $this->container->get(DispatcherFactory::class); 57 | $this->bound = $this->container->has(BoundInterface::class) 58 | ? $this->container->get(BoundInterface::class) 59 | : new ContainerProxy($this->container); 60 | } 61 | 62 | public function __call($name, $arguments) 63 | { 64 | $router = $this->dispatcherFactory->getRouter($this->serverName); 65 | if ($arguments[1] instanceof Closure) { 66 | $arguments[1] = $arguments[1]->bindTo($this->bound, $this->bound); 67 | } 68 | return $router->{$name}(...$arguments); 69 | } 70 | 71 | /** 72 | * Run the application. 73 | */ 74 | public function run(): void 75 | { 76 | $application = $this->container->get(\Hyperf\Contract\ApplicationInterface::class); 77 | $application->run(); 78 | } 79 | 80 | /** 81 | * Config the application using arrays. 82 | */ 83 | public function config(array $configs, int $flag = Constant::CONFIG_MERGE): void 84 | { 85 | foreach ($configs as $key => $value) { 86 | $this->addConfig($key, $value, $flag); 87 | } 88 | } 89 | 90 | /** 91 | * Get the dependency injection container. 92 | */ 93 | public function getContainer(): ContainerInterface 94 | { 95 | return $this->container; 96 | } 97 | 98 | /** 99 | * Add a middleware globally. 100 | */ 101 | public function addMiddleware(callable|MiddlewareInterface|string $middleware): void 102 | { 103 | if ($middleware instanceof MiddlewareInterface || is_string($middleware)) { 104 | $this->appendConfig('middlewares.' . $this->serverName, $middleware); 105 | return; 106 | } 107 | 108 | $middleware = Closure::fromCallable($middleware); 109 | $middlewareFactory = $this->container->get(MiddlewareFactory::class); 110 | $this->appendConfig( 111 | 'middlewares.' . $this->serverName, 112 | $middlewareFactory->create($middleware->bindTo($this->bound, $this->bound)) 113 | ); 114 | } 115 | 116 | /** 117 | * Add an exception handler globally. 118 | */ 119 | public function addExceptionHandler(callable|string $exceptionHandler): void 120 | { 121 | if (is_string($exceptionHandler)) { 122 | $this->appendConfig('exceptions.handler.' . $this->serverName, $exceptionHandler); 123 | return; 124 | } 125 | 126 | $exceptionHandler = Closure::fromCallable($exceptionHandler); 127 | $exceptionHandlerFactory = $this->container->get(ExceptionHandlerFactory::class); 128 | $handler = $exceptionHandlerFactory->create($exceptionHandler->bindTo($this->bound, $this->bound)); 129 | $handlerId = spl_object_hash($handler); 130 | $this->container->set($handlerId, $handler); 131 | $this->appendConfig( 132 | 'exceptions.handler.' . $this->serverName, 133 | $handlerId 134 | ); 135 | } 136 | 137 | /** 138 | * Add an listener globally. 139 | * @param null|callable|string $listener 140 | */ 141 | public function addListener(string $event, $listener = null, int $priority = 1): void 142 | { 143 | if ($listener === null) { 144 | $listener = $event; 145 | } 146 | 147 | if (is_string($listener)) { 148 | $this->appendConfig('listeners', $listener); 149 | return; 150 | } 151 | 152 | $listener = Closure::fromCallable($listener); 153 | $listener = $listener->bindTo($this->bound, $this->bound); 154 | $provider = $this->container->get(ListenerProviderInterface::class); 155 | $provider->on($event, $listener, $priority); 156 | } 157 | 158 | /** 159 | * Add a route group. 160 | */ 161 | public function addGroup(array|string $prefix, callable $callback, array $options = []): void 162 | { 163 | $router = $this->dispatcherFactory->getRouter($this->serverName); 164 | if (isset($options['middleware'])) { 165 | $this->convertClosureToMiddleware($options['middleware']); 166 | } 167 | $router->addGroup($prefix, $callback, $options); 168 | } 169 | 170 | /** 171 | * Add a new command. 172 | * @param null|callable|string $command 173 | */ 174 | public function addCommand(string $signature, $command = null): Command 175 | { 176 | if ($command === null) { 177 | $command = $signature; 178 | } 179 | 180 | if (is_string($command)) { 181 | $this->appendConfig('commands', $command); 182 | return $this->container->get($command); 183 | } 184 | 185 | $command = Closure::fromCallable($command); 186 | /** @var CommandFactory $commandFactory */ 187 | $commandFactory = $this->container->get(CommandFactory::class); 188 | $handler = $commandFactory->create($signature, $command->bindTo($this->bound, $this->bound)); 189 | 190 | return tap( 191 | $handler, 192 | function ($handler) { 193 | $handlerId = spl_object_hash($handler); 194 | $this->container->set($handlerId, $handler); 195 | $this->appendConfig( 196 | 'commands', 197 | $handlerId 198 | ); 199 | } 200 | ); 201 | } 202 | 203 | /** 204 | * Add a new crontab. 205 | */ 206 | public function addCrontab(string $rule, callable|string $crontab): Crontab 207 | { 208 | $this->config->set('crontab.enable', true); 209 | $this->ensureConfigHasValue('processes', CrontabDispatcherProcess::class); 210 | 211 | if ($crontab instanceof Crontab) { 212 | $this->appendConfig('crontab.crontab', $crontab); 213 | return $crontab; 214 | } 215 | 216 | $callback = Closure::fromCallable($crontab); 217 | $callback = $callback->bindTo($this->bound, $this->bound); 218 | $callbackId = spl_object_hash($callback); 219 | $this->container->set($callbackId, $callback); 220 | $this->ensureConfigHasValue('processes', CrontabDispatcherProcess::class); 221 | $this->config->set('crontab.enable', true); 222 | 223 | return tap( 224 | (new Crontab()) 225 | ->setName(uniqid()) 226 | ->setRule($rule) 227 | ->setCallback([CronFactory::class, 'execute', [$callbackId]]), 228 | function ($crontab) { 229 | $this->appendConfig( 230 | 'crontab.crontab', 231 | $crontab 232 | ); 233 | } 234 | ); 235 | } 236 | 237 | /** 238 | * Add a new process. 239 | */ 240 | public function addProcess(callable|string $process): AbstractProcess|ClosureProcess 241 | { 242 | if (is_string($process)) { 243 | $this->appendConfig('processes', $process); 244 | return $this->container->get($process); 245 | } 246 | 247 | $callback = Closure::fromCallable($process); 248 | $callback = $callback->bindTo($this->bound, $this->bound); 249 | $processFactory = $this->container->get(ProcessFactory::class); 250 | 251 | return tap($processFactory->create($callback), function ($process) { 252 | $processId = spl_object_hash($process); 253 | $this->container->set($processId, $process); 254 | $this->appendConfig( 255 | 'processes', 256 | $processId 257 | ); 258 | }); 259 | } 260 | 261 | /** 262 | * Add a new route. 263 | * @param mixed $httpMethod 264 | * @param mixed $handler 265 | */ 266 | public function addRoute($httpMethod, string $route, $handler, array $options = []): void 267 | { 268 | $router = $this->dispatcherFactory->getRouter($this->serverName); 269 | if (isset($options['middleware'])) { 270 | $this->convertClosureToMiddleware($options['middleware']); 271 | } 272 | if ($handler instanceof Closure) { 273 | $handler = $handler->bindTo($this->bound, $this->bound); 274 | } 275 | $router->addRoute($httpMethod, $route, $handler, $options); 276 | } 277 | 278 | /** 279 | * Add a server. 280 | */ 281 | public function addServer(string $serverName, callable $callback): void 282 | { 283 | $this->serverName = $serverName; 284 | call($callback, [$this]); 285 | $this->serverName = 'http'; 286 | } 287 | 288 | private function appendConfig(string $key, $configValues): void 289 | { 290 | $configs = $this->config->get($key, []); 291 | array_push($configs, $configValues); 292 | $this->config->set($key, $configs); 293 | } 294 | 295 | private function ensureConfigHasValue(string $key, $configValues): void 296 | { 297 | $config = $this->config->get($key, []); 298 | if (! is_array($config)) { 299 | return; 300 | } 301 | 302 | if (in_array($configValues, $config)) { 303 | return; 304 | } 305 | 306 | array_push($config, $configValues); 307 | $this->config->set($key, $config); 308 | } 309 | 310 | private function addConfig(string $key, $configValues, $flag): void 311 | { 312 | $config = $this->config->get($key); 313 | 314 | if (! is_array($config)) { 315 | $this->config->set($key, $configValues); 316 | return; 317 | } 318 | 319 | if ($flag === Constant::CONFIG_MERGE) { 320 | $this->config->set($key, array_merge_recursive($config, $configValues)); 321 | } else { 322 | $this->config->set($key, array_merge($config, $configValues)); 323 | } 324 | } 325 | 326 | private function convertClosureToMiddleware(array &$middlewares): void 327 | { 328 | $middlewareFactory = $this->container->get(MiddlewareFactory::class); 329 | foreach ($middlewares as &$middleware) { 330 | if ($middleware instanceof Closure) { 331 | $middleware = $middleware->bindTo($this->bound, $this->bound); 332 | $middleware = $middlewareFactory->create($middleware); 333 | } 334 | } 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/BoundInterface.php: -------------------------------------------------------------------------------- 1 | [ 21 | BoundInterface::class => ContainerProxy::class, 22 | ], 23 | 'commands' => [ 24 | ], 25 | 'annotations' => [ 26 | 'scan' => [ 27 | 'paths' => [ 28 | __DIR__, 29 | ], 30 | ], 31 | ], 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Constant.php: -------------------------------------------------------------------------------- 1 | request = $container->get(RequestInterface::class); 28 | $this->response = $container->get(ResponseInterface::class); 29 | } 30 | 31 | public function __call($name, $arguments) 32 | { 33 | return $this->container->{$name}(...$arguments); 34 | } 35 | 36 | public function get($id) 37 | { 38 | return $this->container->get($id); 39 | } 40 | 41 | public function define(string $name, $definition): void 42 | { 43 | $this->container->define($name, $definition); 44 | } 45 | 46 | public function has($id): bool 47 | { 48 | return $this->container->has($id); 49 | } 50 | 51 | public function make(string $name, array $parameters = []) 52 | { 53 | return $this->container->make($name, $parameters); 54 | } 55 | 56 | public function set(string $name, $entry): void 57 | { 58 | $this->container->set($name, $entry); 59 | } 60 | 61 | public function unbind(string $name): void 62 | { 63 | /* @phpstan-ignore-next-line */ 64 | $this->container->unbind($name); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Factory/AppFactory.php: -------------------------------------------------------------------------------- 1 | config([ 46 | 'server' => Preset::default(), 47 | 'server.servers.0.host' => $host, 48 | 'server.servers.0.port' => $port, 49 | ]); 50 | $app->addExceptionHandler(HttpExceptionHandler::class); 51 | return $app; 52 | } 53 | 54 | /** 55 | * Create a single worker application in base mode, with max_requests = 0. 56 | */ 57 | public static function createBase(string $host = '0.0.0.0', int $port = 9501, array $dependencies = []): App 58 | { 59 | $app = self::createApp($dependencies); 60 | $app->config([ 61 | 'server' => Preset::base(), 62 | 'server.servers.0.host' => $host, 63 | 'server.servers.0.port' => $port, 64 | ]); 65 | $app->addExceptionHandler(HttpExceptionHandler::class); 66 | return $app; 67 | } 68 | 69 | public static function createCoroutine(string $host = '0.0.0.0', int $port = 9501, array $dependencies = []): App 70 | { 71 | $app = self::createApp($dependencies); 72 | $app->config([ 73 | 'server' => Preset::swooleCoroutine(), 74 | 'server.servers.0.host' => $host, 75 | 'server.servers.0.port' => $port, 76 | ]); 77 | $app->addExceptionHandler(HttpExceptionHandler::class); 78 | return $app; 79 | } 80 | 81 | public static function createSwow(string $host = '0.0.0.0', int $port = 9501, array $dependencies = []): App 82 | { 83 | $app = self::createApp($dependencies); 84 | $app->config([ 85 | 'server' => Preset::swow(), 86 | 'server.servers.0.host' => $host, 87 | 'server.servers.0.port' => $port, 88 | ]); 89 | $app->addExceptionHandler(HttpExceptionHandler::class); 90 | return $app; 91 | } 92 | 93 | /** 94 | * Create an application with a chosen preset. 95 | */ 96 | public static function createApp(array $dependencies = []): App 97 | { 98 | // Setting ini and flags 99 | self::prepareFlags(); 100 | 101 | // Load envs 102 | if (file_exists(BASE_PATH . '/.env')) { 103 | self::loadDotenv(); 104 | } 105 | 106 | // Prepare container 107 | $container = self::prepareContainer($dependencies); 108 | 109 | return new App($container); 110 | } 111 | 112 | protected static function prepareContainer(array $dependencies = []): ContainerInterface 113 | { 114 | $config = new Config(ProviderConfig::load()); 115 | $config->set(StdoutLoggerInterface::class, [ 116 | 'log_level' => [ 117 | LogLevel::ALERT, 118 | LogLevel::CRITICAL, 119 | env('APP_DEBUG', false) ? LogLevel::DEBUG : null, 120 | LogLevel::EMERGENCY, 121 | LogLevel::ERROR, 122 | LogLevel::INFO, 123 | LogLevel::NOTICE, 124 | LogLevel::WARNING, 125 | ], 126 | ]); 127 | $dependencies = array_merge($config->get('dependencies', []), $dependencies); 128 | $container = new Container(new DefinitionSource($dependencies)); 129 | $container->set(ConfigInterface::class, $config); 130 | $container->define(DispatcherFactory::class, DispatcherFactory::class); 131 | $container->define(BoundInterface::class, ContainerProxy::class); 132 | 133 | ApplicationContext::setContainer($container); 134 | return $container; 135 | } 136 | 137 | /** 138 | * Setup flags, ini settings and constants. 139 | */ 140 | protected static function prepareFlags(int $hookFlags = SWOOLE_HOOK_ALL): void 141 | { 142 | ini_set('display_errors', 'on'); 143 | ini_set('display_startup_errors', 'on'); 144 | error_reporting(E_ALL); 145 | $reflection = new ReflectionClass(\Composer\Autoload\ClassLoader::class); 146 | $projectRootPath = dirname($reflection->getFileName(), 3); 147 | ! defined('BASE_PATH') && define('BASE_PATH', $projectRootPath); 148 | ! defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', $hookFlags); 149 | } 150 | 151 | /** 152 | * Setup envs. 153 | */ 154 | protected static function loadDotenv(): void 155 | { 156 | $repository = RepositoryBuilder::createWithNoAdapters() 157 | ->addAdapter(PutenvAdapter::class) 158 | ->immutable() 159 | ->make(); 160 | 161 | Dotenv::create($repository, [BASE_PATH])->load(); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Factory/ClosureCommand.php: -------------------------------------------------------------------------------- 1 | parameterParser = $container->get(ParameterParser::class); 28 | parent::__construct(); 29 | } 30 | 31 | public function handle() 32 | { 33 | $inputs = array_merge($this->input->getArguments(), $this->input->getOptions()); 34 | $parameters = $this->parameterParser->parseClosureParameters($this->closure, $inputs); 35 | 36 | call($this->closure->bindTo($this, $this), $parameters); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Factory/ClosureProcess.php: -------------------------------------------------------------------------------- 1 | closure->bindTo($this, $this)); 36 | } 37 | 38 | public function isEnable($server): bool 39 | { 40 | if (is_callable($this->enable)) { 41 | return (bool) call($this->enable, [$server]); 42 | } 43 | 44 | return (bool) $this->enable; 45 | } 46 | 47 | /** 48 | * @param bool|callable $enable 49 | */ 50 | public function setEnable($enable): self 51 | { 52 | $this->enable = $enable; 53 | return $this; 54 | } 55 | 56 | public function setEnableCoroutine(bool $enableCoroutine): self 57 | { 58 | $this->enableCoroutine = $enableCoroutine; 59 | return $this; 60 | } 61 | 62 | public function setName(string $name): self 63 | { 64 | $this->name = $name; 65 | return $this; 66 | } 67 | 68 | public function setNums(int $nums): self 69 | { 70 | $this->nums = $nums; 71 | return $this; 72 | } 73 | 74 | public function setPipeType(int $pipeType): self 75 | { 76 | $this->pipeType = $pipeType; 77 | return $this; 78 | } 79 | 80 | public function setRedirectStdinStdout(bool $redirectStdinStdout): self 81 | { 82 | $this->redirectStdinStdout = $redirectStdinStdout; 83 | return $this; 84 | } 85 | 86 | public function setRestartInterval(int $restartInterval): self 87 | { 88 | $this->restartInterval = $restartInterval; 89 | return $this; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Factory/CommandFactory.php: -------------------------------------------------------------------------------- 1 | container->get($name); 26 | $callback(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Factory/ExceptionHandlerFactory.php: -------------------------------------------------------------------------------- 1 | closure = $closure; 35 | } 36 | 37 | public function handle(Throwable $throwable, ResponseInterface $response) 38 | { 39 | return call($this->closure, [$throwable, $response]); 40 | } 41 | 42 | public function isValid(Throwable $throwable): bool 43 | { 44 | return true; 45 | } 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Factory/MiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | closure = $closure; 36 | } 37 | 38 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 39 | { 40 | return call($this->closure, [$request, $handler]); 41 | } 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Factory/ParameterParser.php: -------------------------------------------------------------------------------- 1 | normalizer = $this->container->get(NormalizerInterface::class); 34 | 35 | if ($this->container->has(ClosureDefinitionCollectorInterface::class)) { 36 | $this->closureDefinitionCollector = $this->container->get(ClosureDefinitionCollectorInterface::class); 37 | } 38 | 39 | if ($this->container->has(MethodDefinitionCollectorInterface::class)) { 40 | $this->methodDefinitionCollector = $this->container->get(MethodDefinitionCollectorInterface::class); 41 | } 42 | } 43 | 44 | /** 45 | * @throws InvalidArgumentException 46 | */ 47 | public function parseClosureParameters(Closure $closure, array $arguments): array 48 | { 49 | if (! $this->closureDefinitionCollector) { 50 | return []; 51 | } 52 | 53 | $definitions = $this->closureDefinitionCollector->getParameters($closure); 54 | 55 | return $this->getInjections($definitions, 'Closure', $arguments); 56 | } 57 | 58 | public function parseMethodParameters(string $class, string $method, array $arguments): array 59 | { 60 | if (! $this->methodDefinitionCollector) { 61 | return []; 62 | } 63 | 64 | $definitions = $this->methodDefinitionCollector->getParameters($class, $method); 65 | return $this->getInjections($definitions, "{$class}::{$method}", $arguments); 66 | } 67 | 68 | /** 69 | * @throws InvalidArgumentException 70 | */ 71 | private function getInjections(array $definitions, string $callableName, array $arguments): array 72 | { 73 | $injections = []; 74 | 75 | foreach ($definitions as $pos => $definition) { 76 | $value = $arguments[$pos] ?? $arguments[$definition->getMeta('name')] ?? $arguments[Str::snake($definition->getMeta('name'), '-')] ?? null; 77 | if ($value === null) { 78 | if ($definition->getMeta('defaultValueAvailable')) { 79 | $injections[] = $definition->getMeta('defaultValue'); 80 | } elseif ($this->container->has($definition->getName())) { 81 | $injections[] = $this->container->get($definition->getName()); 82 | } elseif ($definition->allowsNull()) { 83 | $injections[] = null; 84 | } else { 85 | throw new InvalidArgumentException("Parameter '{$definition->getMeta('name')}' " 86 | . "of {$callableName} should not be null"); 87 | } 88 | } else { 89 | $injections[] = $this->normalizer->denormalize($value, $definition->getName()); 90 | } 91 | } 92 | 93 | return $injections; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Factory/ProcessFactory.php: -------------------------------------------------------------------------------- 1 | SWOOLE_BASE, 17 | 'servers' => [ 18 | [ 19 | 'name' => 'http', 20 | 'type' => Server::SERVER_HTTP, 21 | 'host' => '0.0.0.0', 22 | 'port' => 9501, 23 | 'sock_type' => SWOOLE_SOCK_TCP, 24 | 'callbacks' => [ 25 | Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], 26 | ], 27 | ], 28 | ], 29 | 'settings' => [ 30 | 'enable_coroutine' => true, 31 | 'worker_num' => 1, 32 | 'open_tcp_nodelay' => true, 33 | 'max_coroutine' => 100000, 34 | 'open_http2_protocol' => true, 35 | 'max_request' => 0, 36 | 'socket_buffer_size' => 2 * 1024 * 1024, 37 | 'buffer_output_size' => 2 * 1024 * 1024, 38 | ], 39 | 'callbacks' => [ 40 | Event::ON_BEFORE_START => [Hyperf\Framework\Bootstrap\ServerStartCallback::class, 'beforeStart'], 41 | Event::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], 42 | Event::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], 43 | Event::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], 44 | ], 45 | ]; 46 | -------------------------------------------------------------------------------- /src/Preset/Default.php: -------------------------------------------------------------------------------- 1 | SWOOLE_PROCESS, 20 | 'servers' => [ 21 | [ 22 | 'name' => 'http', 23 | 'type' => Server::SERVER_HTTP, 24 | 'host' => '0.0.0.0', 25 | 'port' => 9501, 26 | 'sock_type' => SWOOLE_SOCK_TCP, 27 | 'callbacks' => [ 28 | Event::ON_REQUEST => [\Hyperf\HttpServer\Server::class, 'onRequest'], 29 | ], 30 | ], 31 | ], 32 | 'settings' => [ 33 | 'enable_coroutine' => true, 34 | 'worker_num' => swoole_cpu_num(), 35 | 'open_tcp_nodelay' => true, 36 | 'max_coroutine' => 100000, 37 | 'open_http2_protocol' => true, 38 | 'max_request' => 100000, 39 | 'socket_buffer_size' => 2 * 1024 * 1024, 40 | 'buffer_output_size' => 2 * 1024 * 1024, 41 | ], 42 | 'callbacks' => [ 43 | Event::ON_BEFORE_START => [\Hyperf\Framework\Bootstrap\ServerStartCallback::class, 'beforeStart'], 44 | Event::ON_WORKER_START => [\Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], 45 | Event::ON_WORKER_EXIT => [\Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], 46 | Event::ON_PIPE_MESSAGE => [\Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], 47 | ], 48 | ]; 49 | -------------------------------------------------------------------------------- /src/Preset/Preset.php: -------------------------------------------------------------------------------- 1 | SWOOLE_BASE, 21 | 'type' => CoroutineServer::class, 22 | 'servers' => [ 23 | [ 24 | 'name' => 'http', 25 | 'type' => Server::SERVER_HTTP, 26 | 'host' => '0.0.0.0', 27 | 'port' => 9501, 28 | 'sock_type' => SWOOLE_SOCK_TCP, 29 | 'callbacks' => [ 30 | Event::ON_REQUEST => [\Hyperf\HttpServer\Server::class, 'onRequest'], 31 | ], 32 | ], 33 | ], 34 | 'settings' => [ 35 | 'enable_coroutine' => true, 36 | 'worker_num' => 1, 37 | 'open_tcp_nodelay' => true, 38 | 'max_coroutine' => 100000, 39 | 'open_http2_protocol' => true, 40 | 'max_request' => 0, 41 | 'socket_buffer_size' => 2 * 1024 * 1024, 42 | 'buffer_output_size' => 2 * 1024 * 1024, 43 | ], 44 | 'callbacks' => [ 45 | Event::ON_BEFORE_START => [\Hyperf\Framework\Bootstrap\ServerStartCallback::class, 'beforeStart'], 46 | Event::ON_WORKER_START => [\Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], 47 | Event::ON_WORKER_EXIT => [\Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], 48 | Event::ON_PIPE_MESSAGE => [\Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], 49 | ], 50 | ]; 51 | -------------------------------------------------------------------------------- /src/Preset/Swow.php: -------------------------------------------------------------------------------- 1 | SwowServer::class, 22 | 'servers' => [ 23 | [ 24 | 'name' => 'http', 25 | 'type' => Server::SERVER_HTTP, 26 | 'host' => '0.0.0.0', 27 | 'port' => 9764, 28 | 'callbacks' => [ 29 | Event::ON_REQUEST => [HttpServer::class, 'onRequest'], 30 | ], 31 | ], 32 | ], 33 | ]; 34 | --------------------------------------------------------------------------------