最近几年在 javascript、golang 生态中游走,发现很多 npm、go mod 的优点。最近回过头开发 MixPHP V3 ,发现 composer 其实一直都是一个非常优秀的工具,但是 phper 们对 composer 的用法很多都不是很深入,今天我就采用 composer 手撸一个原生项目,帮助大家理解现代化的原生 PHP 开发流程。 PHP 的开发者可能是所有语言里被惯坏的最厉害的,因为几乎每个框架都提供了脚手架,像这样: composer create-project
这个在 npm、go mod 是没有这个功能的,需要自己创建程序骨架,当然 npm 和 go 生态产生了自己的解决方案,就是 vue-cli 和 mixcli 这样的脚手架工具来负责创建。 创建一个项目和 npm init 、go mod init 一样,我们使用 composer init 创建一个项目 mkdir hellocd hellocomposer init
交互式填写一些内容后,生成了 composer.json 文件 { "name": "liujian/hello", "type": "project", "autoload": { "psr-4": { "Liujian\\Hello\\": "src/" } }, "require": {}}
这个文件是以 composer 库的标准创建的,必须要两级名称,这让我很蛋疼,所以我修改一下 { "name": "project/app", "type": "project", "autoload": { "psr-4": { "App\\": "src/" } }, "require": {}}
选择我需要使用的库和 node.js、go 生态一样,第二步就是寻找我们需要的库,通常我们的需求是写一个 API 服务,就需要一个 http server 库,一个 db 库就可以开始工作了。 由于是现代化的 PHP 开发,因此我选择了 PHP CLI 模式的常驻高性能库,这里我选择的是: - mix/vega Vega 是一个用 PHP 编写的 CLI 模式 HTTP 网络框架,支持 Swoole、WorkerMan
- mix/database 可在各种环境中使用的轻量数据库,支持 FPM、Swoole、WorkerMan,可选的连接池 (协程)
这两个都是 MixPHP V3+ 的核心组件。 Mix Vega & Mix Database 安装Vega 同时支持 Swoole、WorkerMan,以后还会支持 Swow,最简单原则,因为 WorkerMan 可以不需要安装扩展即可执行,开发先采用 WorkerMan 来驱动 Vega,上线可根据自己的需要切换。 安装 Workerman composer require workerman/workerman
安装 Mix Vega composer require mix/vega
安装 Mix Database composer require mix/database
创建一个入口文件vue 的入口通常是 src/main.js 因为 js 通常是单入口项目,我们还是按二进制的惯例,创建一个 bin/start.php 入口文件 <?phprequire __DIR__ . '/../vendor/autoload.php';$vega = new Mix\Vega\Engine();$vega->handleF('/hello', function (Mix\Vega\Context $ctx) { $ctx->string(200, 'hello, world!');})->methods('GET');$http_worker = new Workerman\Worker("http://0.0.0.0:2345");$http_worker->onMessage = $vega->handler();$http_worker->count = 4;Workerman\Worker::runAll();
然后我们模仿 npm 的搞法,在 composer.json 增加: "scripts": { "server": "php bin/start.php start"},
这里我非常困惑 composer 的搞法,npm 的入口文件中可不需要 require __DIR__ . '/../vendor/autoload.php'; 直接 npm run server 执行的脚本是自己可以找到对应依赖的,但是 composer 即便使用 composer run server 执行对应的脚本,依然要在代码里处理 autoload,给差评。 现在我们 composer run server 启动服务试试: % composer run server> php8 bin/start.php startWorkerman[bin/start.php] start in DEBUG mode----------------------------------------- WORKERMAN -----------------------------------------Workerman version:4.0.19 PHP version:8.0.7------------------------------------------ WORKERS ------------------------------------------proto user worker listen processes status tcp liujian none http://0.0.0.0:2345 4 [OK] ---------------------------------------------------------------------------------------------Press Ctrl+C to stop. Start success.
写一个 API 接口我们将上面的入口文件改造一下,写一个用户查询接口,Vega 的使用非常简单。 <?phprequire __DIR__ . '/../vendor/autoload.php';const DSN = 'mysql:host=127.0.0.1;port=3306;charset=utf8;dbname=test';const USERNAME = 'root';const PASSWORD = '123456';$db = new \Mix\Database\Database(DSN, USERNAME, PASSWORD);$vega = new Mix\Vega\Engine();$vega->handleF('/users/{id}', function (Mix\Vega\Context $ctx) use ($db) { $row = $db->table('users')->where('id = ?', $ctx->param('id'))->first(); if (!$row) { throw new \Exception('User not found'); } $ctx->JSON(200, [ 'code' => 0, 'message' => 'ok', 'data' => $row ]);})->methods('GET');$http_worker = new Workerman\Worker("http://0.0.0.0:2345");$http_worker->onMessage = $vega->handler();$http_worker->count = 4;Workerman\Worker::runAll();
curl 测试一下: % curl http://127.0.0.1:2345/users/1{"code":0,"message":"ok","data":{"id":"1","name":"foo2","balance":"102","add_time":"2021-07-06 08:40:20"}}
使用 PSR 调整一下目录结构前面我们定义了 PSR "autoload": { "psr-4": { "App\\": "src/" }},
接下来我们采用自动加载来合理拆分上面入口文件的代码,拆分后目录结构如下: ├── bin│ └── start.php├── composer.json├── composer.lock├── src│ ├── Controller│ │ └── Users.php│ ├── Database│ │ └── DB.php│ └── Router│ └── Vega.php└── vendor
<?phprequire __DIR__ . '/../vendor/autoload.php';$vega = \App\Router\Vega::new();$http_worker = new Workerman\Worker("http://0.0.0.0:2345");$http_worker->onMessage = $vega->handler();$http_worker->count = 8;Workerman\Worker::runAll();
<?phpnamespace App\Router;use App\Controller\Users;use Mix\Vega\Engine;class Vega{ /** * @return Engine */ public static function new() { $vega = new Engine(); $vega->handleC('/users/{id}', [new Users(), 'index'])->methods('GET'); return $vega; }}
<?phpnamespace App\Controller;use App\Database\DB;use Mix\Vega\Context;class Users{ public function index(Context $ctx) { $row = DB::instance()->table('users')->where('id = ?', $ctx->param('id'))->first(); if (!$row) { throw new \Exception('User not found'); } $ctx->JSON(200, [ 'code' => 0, 'message' => 'ok', 'data' => $row ]); }}
<?phpnamespace App\Database;use Mix\Database\Database;const DSN = 'mysql:host=127.0.0.1;port=3306;charset=utf8;dbname=test';const USERNAME = 'root';const PASSWORD = '123456';class DB extends Database{ static private $instance; public static function instance() { if (!isset(self::$instance)) { self::$instance = new self(DSN, USERNAME, PASSWORD); } return self::$instance; }}
调整完基本就完成了一个正式项目的雏形了,接下来大家可以自由发挥。 压测一下mysql: docker mysql8 本机 cpu: macOS M1 8核 mem: 16G wokerman (未安装libevent): 8进程,相当于8个mysql连接 % wrk -c 1000 -d 1m http://127.0.0.1:2345/users/1Running 1m test @ http://127.0.0.1:2345/users/1 2 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 36.08ms 8.11ms 428.09ms 95.38% Req/Sec 3.49k 211.80 4.00k 71.00% 416817 requests in 1.00m, 109.31MB read Socket errors: connect 749, read 295, write 1, timeout 0Requests/sec: 6943.38Transfer/sec: 1.82MB |