├── .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 |
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 | 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 |