使用 mixvega + mixdb 進行現代化的原生 PHP 開發

最近幾年在 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

bin/start。php

<?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();

src/Router/Vega。php

<?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; }}

src/Controller/Users。php

<?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 ]); }}

src/Database/DB。php

<?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